上下文管理器对象存在的目的是管理 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 上下文管理器(https://docs.python.org/3/library/contextlib.html#contextlib.redirect_stdout)就是这么做的:只需传入类似文件的对象,用于替代 sys.stdout

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

exc_type

  异常类(例如 ZeroDivisionError)。

exc_value

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

traceback

  traceback 对象。3

3try/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 社区肯定还在不断寻找新的创意用法。标准库中有一些示例。

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

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