- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- 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.2 上下文管理器和 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 上下文管理器就是这么做的:只需传入类似文件的对象,用于替代 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论