@contextmanager 装饰器能减少创建上下文管理器的样板代码量,因 为不用编写一个完整的类,定义 __enter__ 和 __exit__ 方法,而只 需实现有一个 yield 语句的生成器,生成想让 __enter__ 方法返回的 值。
在使用 @contextmanager 装饰的生成器中,yield 语句的作用是把函 数的定义体分成两部分:yield 语句前面的所有代码在 with 块开始时 (即解释器调用 __enter__ 方法时)执行, yield 语句后面的代码在 with 块结束时(即调用 __exit__ 方法时)执行。
下面举个例子。示例 15-5 使用一个生成器函数代替示例 15-3 中定义的 LookingGlass 类。 示例 15-5 mirror_gen.py:使用生成器实现的上下文管理器
❶ 应用 contextmanager 装饰器。
❷ 贮存原来的 sys.stdout.write 方法。
❸ 定义自定义的 reverse_write 函数;在闭包中可以访问 original_write。
❹ 把 sys.stdout.write 替换成 reverse_write。
❺ 产出一个值,这个值会绑定到 with 语句中 as 子句的目标变量上。
执行 with 块中的代码时,这个函数会在这一点暂停。
❻ 控制权一旦跳出 with 块,继续执行 yield 语句之后的代码;这里是 恢复成原来的 sys. stdout.write 方法。
示例 15-6 是使用 looking_glass 函数的例子。 示例 15-6 测试 looking_glass 上下文管理器函数
➊ 与示例 15-2 唯一的区别是上下文管理器的名字:LookingGlass 变 成了 looking_glass。 其实,contextlib.contextmanager 装饰器会把函数包装成实现__enter__ 和 __exit__ 方法的类。
这个类的 __enter__ 方法有如下作用。
(1) 调用生成器函数,保存生成器对象(这里把它称为 gen)。
(2) 调用 next(gen),执行到 yield 关键字所在的位置。
(3) 返回 next(gen) 产出的值,以便把产出的值绑定到 with/as 语句 中的目标变量上。
with 块终止时,__exit__ 方法会做以下几件事。
(1) 检查有没有把异常传给 exc_type;如果有,调用 gen.throw(exception),在生成器函数定义体中包含 yield 关键字 的那一行抛出异常。
(2) 否则,调用 next(gen),继续执行生成器函数定义体中 yield 语句 之后的代码。
示例 15-5 有一个严重的错误:如果在 with 块中抛出了异常,Python 解 释器会将其捕获,然后在 looking_glass 函数的 yield 表达式里再次 抛出。但是,那里没有处理错误的代码,因此 looking_glass 函数会 中止,永远无法恢复成原来的 sys.stdout.write 方法,导致系统处 于无效状态。
示例 15-7 添加了一些代码,特别用于处理 ZeroDivisionError 异常; 这样,在功能上它就与示例 15-3 中基于类的实现等效了。
示例 15-7 mirror_gen_exc.py:基于生成器的上下文管理器,而且 实现了异常处理——从外部看,行为与示例 15-3 一样
❶ 创建一个变量,用于保存可能出现的错误消息;与示例 15-5 相比, 这是第一处改动。
❷ 处理 ZeroDivisionError 异常,设置一个错误消息。
❸ 撤销对 sys.stdout.write 方法所做的猴子补丁。
❹ 如果设置了错误消息,把它打印出来。
前面说过,为了告诉解释器异常已经处理了,__exit__ 方法会返回 True,此时解释器会压制异常。如果 __exit__ 方法没有显式返回一个 值,那么解释器得到的是 None,然后向上冒泡异常。使用 @contextmanager 装饰器时,默认的行为是相反的:装饰器提供的__exit__ 方法假定发给生成器的所有异常都得到处理了,因此应该压 制异常。 如果不想让 @contextmanager 压制异常,必须在被装饰的 函数中显式重新抛出异常。
使用 @contextmanager 装饰器时,要把 yield 语句放在 try/finally 语句中(或者放在 with 语句中),这是无法避免 的,因为我们永远不知道上下文管理器的用户会在 with 块中做什 么。
除了标准库中举的例子之外,Martijn Pieters 实现的原地文件重写上下文 管理器(http://www.zopatista.com/python/2013/11/26/inplace-file-rewriting/)是 @contextmanager 不错的使用实例。用法如示例 15-8 所 示。
示例 15-8 用于原地重写文件的上下文管理器
inplace 函数是个上下文管理器,为同一个文件提供了两个句柄(这个 示例中的 infh 和 outfh),以便同时读写同一个文件。这比标准库中 的 fileinput.input 函数 (https://docs.python.org/3/library/fileinput.html#fileinput.input;顺便说一 下,这个函数也提供了一个上下文管理器)易于使用。
如果想学习 Martijn 实现 inplace 的源码(列在这篇文章 中:http://www.zopatista.com/python/2013/11/26/inplace-file-rewriting/), 找到 yield 关键字,在此之前的所有代码都用于设置上下文:先创建 备份文件,然后打开并产出 __enter__ 方法返回的可读和可写文件句 柄的引用。yield 关键字之后的 __exit__ 处理过程把文件句柄关闭; 如果什么地方出错了,那么从备份中恢复文件。
注意,在 @contextmanager 装饰器装饰的生成器中,yield 与迭代没 有任何关系。在本节所举的示例中,生成器函数的作用更像是协程:执 行到某一点时暂停,让客户代码运行,直到客户让协程继续做事。第 16 章会全面讨论协程。
以上就是“Python 上下文管理器和 else 块(使用@contextmanager)”的详细内容,想要了解更多Python教程欢迎持续关注编程学习网。
扫码二维码 获取免费视频学习资料
- 本文固定链接: http://phpxs.com/post/13240/
- 转载请注明:转载必须在正文中标注并保留原文链接
- 扫码: 扫上方二维码获取免费视频资料