- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- Python 术语表
- Python 版本表
- 排版约定
- 使用代码示例
- 第一部分 序幕
- 第 1 章 Python 数据模型
- 第二部分 数据结构
- 第 2 章 序列构成的数组
- 第 3 章 字典和集合
- 第 4 章 文本和字节序列
- 第三部分 把函数视作对象
- 第 5 章 一等函数
- 第 6 章 使用一等函数实现设计模式
- 第 7 章 函数装饰器和闭包
- 第四部分 面向对象惯用法
- 第 8 章 对象引用、可变性和垃圾回收
- 第 9 章 符合 Python 风格的对象
- 第 10 章 序列的修改、散列和切片
- 第 11 章 接口:从协议到抽象基类
- 第 12 章 继承的优缺点
- 第 13 章 正确重载运算符
- 第五部分 控制流程
- 第 14 章 可迭代的对象、迭代器和生成器
- 14.1 Sentence 类第1版:单词序列
- 14.2 可迭代的对象与迭代器的对比
- 14.3 Sentence 类第2版:典型的迭代器
- 14.4 Sentence 类第3版:生成器函数
- 14.5 Sentence 类第4版:惰性实现
- 14.6 Sentence 类第5版:生成器表达式
- 14.7 何时使用生成器表达式
- 14.8 另一个示例:等差数列生成器
- 14.9 标准库中的生成器函数
- 14.10 Python 3.3 中新出现的句法:yield from
- 14.11 可迭代的归约函数
- 14.12 深入分析 iter 函数
- 14.13 案例分析:在数据库转换工具中使用生成器
- 14.14 把生成器当成协程
- 14.15 本章小结
- 14.16 延伸阅读
- 第 15 章 上下文管理器和 else 块
- 第 16 章 协程
- 第 17 章 使用期物处理并发
- 第 18 章 使用 asyncio 包处理并发
- 第六部分 元编程
- 第 19 章 动态属性和特性
- 第 20 章 属性描述符
- 第 21 章 类元编程
- 结语
- 延伸阅读
- 附录 A 辅助脚本
- Python 术语表
- 作者简介
- 关于封面
15.4 使用 @contextmanager
@contextmanager 装饰器能减少创建上下文管理器的样板代码量,因为不用编写一个完整的类,定义 __enter__ 和 __exit__ 方法,而只需实现有一个 yield 语句的生成器,生成想让 __enter__ 方法返回的值。
在使用 @contextmanager 装饰的生成器中,yield 语句的作用是把函数的定义体分成两部分:yield 语句前面的所有代码在 with 块开始时(即解释器调用 __enter__ 方法时)执行, yield 语句后面的代码在 with 块结束时(即调用 __exit__ 方法时)执行。
下面举个例子。示例 15-5 使用一个生成器函数代替示例 15-3 中定义的 LookingGlass 类。
示例 15-5 mirror_gen.py:使用生成器实现的上下文管理器
import contextlib @contextlib.contextmanager ➊ def looking_glass(): import sys original_write = sys.stdout.write ➋ def reverse_write(text): ➌ original_write(text[::-1]) sys.stdout.write = reverse_write ➍ yield 'JABBERWOCKY' ➎ sys.stdout.write = original_write ➏
❶ 应用 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 上下文管理器函数
>>> from mirror_gen import looking_glass >>> with looking_glass() as what: ➊ ... print('Alice, Kitty and Snowdrop') ... print(what) ... pordwonS dna yttiK ,ecilA YKCOWREBBAJ >>> what 'JABBERWOCKY'
➊ 与示例 15-2 唯一的区别是上下文管理器的名字:LookingGlass 变成了 looking_glass。
其实,contextlib.contextmanager 装饰器会把函数包装成实现 __enter__ 和 __exit__ 方法的类。5
5类的名称是 _GeneratorContextManager。如果想了解具体的工作方式,可以阅读 Python 3.4 发行版中 Lib/contextlib.py 文件里的源码。
这个类的 __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 一样
import contextlib @contextlib.contextmanager def looking_glass(): import sys original_write = sys.stdout.write def reverse_write(text): original_write(text[::-1]) sys.stdout.write = reverse_write msg = '' ➊ try: yield 'JABBERWOCKY' except ZeroDivisionError: ➋ msg = 'Please DO NOT divide by zero!' finally: sys.stdout.write = original_write ➌ if msg: print(msg) ➍
❶ 创建一个变量,用于保存可能出现的错误消息;与示例 15-5 相比,这是第一处改动。
❷ 处理 ZeroDivisionError 异常,设置一个错误消息。
❸ 撤销对 sys.stdout.write 方法所做的猴子补丁。
❹ 如果设置了错误消息,把它打印出来。
前面说过,为了告诉解释器异常已经处理了,__exit__ 方法会返回 True,此时解释器会压制异常。如果 __exit__ 方法没有显式返回一个值,那么解释器得到的是 None,然后向上冒泡异常。使用 @contextmanager 装饰器时,默认的行为是相反的:装饰器提供的 __exit__ 方法假定发给生成器的所有异常都得到处理了,因此应该压制异常。6 如果不想让 @contextmanager 压制异常,必须在被装饰的函数中显式重新抛出异常。7
6把异常发给生成器的方式是使用 throw 方法,参见 16.5 节。
7这样约定的原因是,创建上下文管理器时,生成器无法返回值,只能产出值。不过,现在可以返回值了,如 16.6 节所述。届时你会看到,如果在生成器中返回值,那么会抛出异常。
使用 @contextmanager 装饰器时,要把 yield 语句放在 try/finally 语句中(或者放在 with 语句中),这是无法避免的,因为我们永远不知道上下文管理器的用户会在 with 块中做什么。8
8这条提示直接引用 Leonardo Rochael 的评论,他是本书的技术审校之一。说得好,Leo !
除了标准库中举的例子之外,Martijn Pieters 实现的原地文件重写上下文管理器是 @contextmanager 不错的使用实例。用法如示例 15-8 所示。
示例 15-8 用于原地重写文件的上下文管理器
import csv with inplace(csvfilename, 'r', newline='') as (infh, outfh): reader = csv.reader(infh) writer = csv.writer(outfh) for row in reader: row += ['new', 'columns'] writer.writerow(row)
inplace 函数是个上下文管理器,为同一个文件提供了两个句柄(这个示例中的 infh 和 outfh),以便同时读写同一个文件。这比标准库中的 fileinput.input 函数;顺便说一下,这个函数也提供了一个上下文管理器)易于使用。
如果想学习 Martijn 实现 inplace 的源码(列在这篇文章中),找到 yield 关键字,在此之前的所有代码都用于设置上下文:先创建备份文件,然后打开并产出 __enter__ 方法返回的可读和可写文件句柄的引用。yield 关键字之后的 __exit__ 处理过程把文件句柄关闭;如果什么地方出错了,那么从备份中恢复文件。
注意,在 @contextmanager 装饰器装饰的生成器中,yield 与迭代没有任何关系。在本节所举的示例中,生成器函数的作用更像是协程:执行到某一点时暂停,让客户代码运行,直到客户让协程继续做事。第 16 章会全面讨论协程。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论