返回介绍

15.2 上下文管理器和 with 块

发布于 2024-02-05 21:59:47 字数 6893 浏览 0 评论 0 收藏 0

上下文管理器对象存在的目的是管理 with 语句,就像迭代器的存在是为了管理 for 语句一样。

with 语句的目的是简化 try/finally 模式。这种模式用于保证一段代码运行完毕后执行某项操作,即便那段代码由于异常、return 语句或 sys.exit() 调用而中止,也会执行指定的操作。finally 子句中的代码通常用于释放重要的资源,或者还原临时变更的状态。

上下文管理器协议包含 __enter__ 和 __exit__ 两个方法。with 语句开始运行时,会在上下文管理器对象上调用 __enter__ 方法。with 语句运行结束后,会在上下文管理器对象上调用 __exit__ 方法,以此扮演 finally 子句的角色。

最常见的例子是确保关闭文件对象。使用 with 语句关闭文件的详细说明参见示例 15-1。

示例 15-1 演示把文件对象当成上下文管理器使用

>>> with open('mirror.py') as fp:  # ➊
...   src = fp.read(60)  # ➋
...
>>> len(src)
60
>>> fp  # ➌
<_io.TextIOWrapper name='mirror.py' mode='r' encoding='UTF-8'>
>>> fp.closed, fp.encoding  # ➍
(True, 'UTF-8')
>>> fp.read(60)  # ➎
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file.

❶ fp 绑定到打开的文件上,因为文件的 __enter__ 方法返回 self。

❷ 从 fp 中读取一些数据。

❸ fp 变量仍然可用。2

2与函数和模块不同,with 块没有定义新的作用域。

❹ 可以读取 fp 对象的属性。

❺ 但是不能在 fp 上执行 I/O 操作,因为在 with 块的末尾,调用 TextIOWrapper.__exit__ 方法把文件关闭了。

示例 15-1 中标注❶的那行代码道出了不易察觉但很重要的一点:执行 with 后面的表达式得到的结果是上下文管理器对象,不过,把值绑定到目标变量上(as 子句)是在上下文管理器对象上调用 __enter__ 方法的结果。

碰巧,示例 15-1 中的 open() 函数返回 TextIOWrapper 类的实例,而该实例的 __enter__ 方法返回 self。不过,__enter__ 方法除了返回上下文管理器之外,还可能返回其他对象。

不管控制流程以哪种方式退出 with 块,都会在上下文管理器对象上调用 __exit__ 方法,而不是在 __enter__ 方法返回的对象上调用。

with 语句的 as 子句是可选的。对 open 函数来说,必须加上 as 子句,以便获取文件的引用。不过,有些上下文管理器会返回 None,因为没什么有用的对象能提供给用户。

示例 15-2 使用一个精心制作的上下文管理器执行操作,以此强调上下文管理器与 __enter__ 方法返回的对象之间的区别。

示例 15-2 测试 LookingGlass 上下文管理器类

  >>> from mirror import LookingGlass
  >>> with LookingGlass() as what:  ➊
  ...    print('Alice, Kitty and Snowdrop')  ➋
  ...    print(what)
  ...
  pordwonS dna yttiK ,ecilA  ➌
  YKCOWREBBAJ
  >>> what  ➍
  'JABBERWOCKY'
  >>> print('Back to normal.')  ➎
  Back to normal.

❶ 上下文管理器是 LookingGlass 类的实例;Python 在上下文管理器上调用 __enter__ 方法,把返回结果绑定到 what 上。

❷ 打印一个字符串,然后打印 what 变量的值。

❸ 打印出的内容是反向的。

❹ 现在,with 块已经执行完毕。可以看出,__enter__ 方法返回的值——即存储在 what 变量中的值——是字符串 'JABBERWOCKY'。

❺ 输出不再是反向的了。

示例 15-3 是 LookingGlass 类的实现。

示例 15-3 mirror.py:LookingGlass 上下文管理器类的代码

class LookingGlass:

  def __enter__(self):  ➊
    import sys
    self.original_write = sys.stdout.write  ➋
    sys.stdout.write = self.reverse_write  ➌
    return 'JABBERWOCKY'  ➍

  def reverse_write(self, text):  ➎
    self.original_write(text[::-1])

  def __exit__(self, exc_type, exc_value, traceback):  ➏
    import sys  ➐
    sys.stdout.write = self.original_write  ➑
    if exc_type is ZeroDivisionError:  ➒
      print('Please DO NOT divide by zero!')
      return True  ➓
    ⓫

❶ 除了 self 之外,Python 调用 __enter__ 方法时不传入其他参数。

❷ 把原来的 sys.stdout.write 方法保存在一个实例属性中,供后面使用。

❸ 为 sys.stdout.write 打猴子补丁,替换成自己编写的方法。

❹ 返回 'JABBERWOCKY' 字符串,这样才有内容存入目标变量 what。

❺ 这是用于取代 sys.stdout.write 的方法,把 text 参数的内容反转,然后调用原来的实现。

❻ 如果一切正常,Python 调用 __exit__ 方法时传入的参数是 None, None, None;如果抛出了异常,这三个参数是异常数据,如下所述。

❼ 重复导入模块不会消耗很多资源,因为 Python 会缓存导入的模块。

❽ 还原成原来的 sys.stdout.write 方法。

❾ 如果有异常,而且是 ZeroDivisionError 类型,打印一个消息……

❿ ……然后返回 True,告诉解释器,异常已经处理了。

⓫ 如果 __exit__ 方法返回 None,或者 True 之外的值,with 块中的任何异常都会向上冒泡。

 在实际使用中,如果应用程序接管了标准输出,可能会暂时把 sys.stdout 换成类似文件的其他对象,然后再切换成原来的版本。contextlib.redirect_stdout 上下文管理器就是这么做的:只需传入类似文件的对象,用于替代 sys.stdout。

解释器调用 __enter__ 方法时,除了隐式的 self 之外,不会传入任何参数。传给 __exit__ 方法的三个参数列举如下。

exc_type

异常类(例如 ZeroDivisionError)。

exc_value

异常实例。有时会有参数传给异常构造方法,例如错误消息,这些参数可以使用 exc_value.args 获取。

traceback

traceback 对象。3

3在 try/finally 语句的 finally 块中调用 sys.exc_info()(https://docs.python.org/3/library/sys.html#sys.exc_info),得到的就是 __exit__ 接收的这三个参数。鉴于 with 语句是为了取代大多数 try/finally 语句,而且通常需要调用 sys.exc_info() 来判断做什么清理操作,这种行为是合理的。

上下文管理器的具体工作方式参见示例 15-4。在这个示例中,我们在 with 块之外使用 LookingGlass 类,因此可以手动调用 __enter__ 和 __exit__ 方法。

示例 15-4 在 with 块之外使用 LookingGlass 类

  >>> from mirror import LookingGlass
  >>> manager = LookingGlass()  ➊
  >>> manager
  <mirror.LookingGlass object at 0x2a578ac>
  >>> monster = manager.__enter__()  ➋
  >>> monster == 'JABBERWOCKY'  ➌
  eurT
  >>> monster
  'YKCOWREBBAJ'
  >>> manager
  >ca875a2x0 ta tcejbo ssalGgnikooL.rorrim<
  >>> manager.__exit__(None, None, None)  ➍
  >>> monster
  'JABBERWOCKY'

❶ 实例化并审查 manager 实例。

❷ 在上下文管理器上调用 __enter__() 方法,把结果存储在 monster 中。

❸ monster 的值是字符串 'JABBERWOCKY'。打印出的 True 标识符是反向的,因为 stdout 的所有输出都经过 __enter__ 方法中打补丁的 write 方法处理。

❹ 调用 manager.__exit__,还原成之前的 stdout.write。

上下文管理器是相当新颖的特性,Python 社区肯定还在不断寻找新的创意用法。标准库中有一些示例。

在 sqlite3 模块中用于管理事务,参见“12.6.7.3. Using the connection as a context manager”。4

在 threading 模块中用于维护锁、条件和信号,参见“17.1.10. Using locks, conditions, and semaphores in the with statement”。

为 Decimal 对象的算术运算设置环境,参见 decimal.localcontext 函数的文档

为了测试临时给对象打补丁,参见 unittest.mock.patch 函数的文档

4在 Python 3.5 文档中是“12.6.8.3”。——编者注

标准库中还有个 contextlib 模块,提供一些实用工具,参见下一节。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文