with块上下文管理器对象存在的目的是管理 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
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”(https://docs.python.org/3/library/sqlite3.html#using-the-connection-as-a-context-manager)。4
在 threading 模块中用于维护锁、条件和信号,参见“17.1.10. Using locks, conditions, and semaphores in the with statement”(https://docs.python.org/3/library/threading.html#using-locks-conditions-and-semaphores-in-the-with-statement)。
为 Decimal 对象的算术运算设置环境,参见 decimal.localcontext 函数的文档(https://docs.python.org/3/library/decimal.html#decimal.localcontext)。
为了测试临时给对象打补丁,参见 unittest.mock.patch 函数的文档(https://docs.python.org/3/library/unittest.mock.html#patch)。
4在 Python 3.5 文档中是“12.6.8.3”。——编者注
标准库中还有个 contextlib 模块,提供一些实用工具,参见下一节。