上下文管理器和 else 块
本章内容
- for、while 和 try 语句的 else 子句
- EAFP 与 LBYL
- 上下文管理器协议
- with 语句和上下文管理器
- contextlib 模块
1. else 子句
作用:
- 能让代码更易于阅读
- 不用设置控制标志或者添加额外的 if 语句
控制逻辑:
- for :仅当 for 循环运行完毕时(没有被 break 语句中止)才运行 else 块
- while:仅当 while 循环因为条件为假值而退出时(没有被 break 语句中止)才运行 else 块
- try:
- 仅当 try 块中没有异常抛出时才运行 else 块
- else 子句抛出的异常不会由前面的 except 子句处理
- 如果异常、return、break 或 continue 导致控制权跳到了复合语句的主块之外,else 子句就会被跳过
|
|
示例分析:
- try 块中应该只捕获预期的异常
- 这里捕获的是 dangerous_call() 可能出现的错误,而不是 after_call()
- 只有 try 块不抛出异常,才会执行 after_call()
2. EAFP 与 LBYL
Python 官方词汇表:https://docs.python.org/3/glossary.html#term-eafp
EAFP:
- 取得原谅比获得许可容易(easier to ask for forgiveness than permission)。这是一 种常见的 Python 编程风格,先假定存在有效的键或属性,如果假定不成立,那 么捕获异常。这种风格简单明快,特点是代码中有很多 try 和 except 语句。与 其他很多语言一样(如 C 语言),这种风格的对立面是 LBYL 风格。
LBYL:
- 三思而后行(look before you leap)。这种编程风格在调用函数或查找属性或键之 前显式测试前提条件。与 EAFP 风格相反,这种风格的特点是代码中有很多 if 语句。在多线程环境中, LBYL 风格可能会在“检查”和“行事”的空当引入条 件竞争。例如,对 if key in mapping: return mapping[key] 这段代码来说,如 果在测试之后,但在查找之前,另一个线程从映射中删除了那个键,那么这段代 码就会失败。这个问题可以使用锁或者 EAFP 风格解决
2. 上下文管理器和with
上下文管理器对象存在的目的是管理 with 语句,就像迭代器的存在是为了管理 for 语句一样
2.1 with 语句
- 作用:
- 简化 try/finally 模式
- 这种模式用于保证一段代码运行完毕后执行某项操作,
- 即便那段代码由于异常、return 语句或 sys.exit() 调用而中止,也会执行指定的操作
- finally 子句中的代码通常用于释放重要的资源,或者还原临时变更的状态
- 不仅能管理资源,还能用于去掉常规的设置和清理代码,或者在另一个过程前后执行的操作
- 特性:与函数和模块不同, with 块没有定义新的作用域
- 原理:
- with 语句会设置一个临时的上下文,交给上下文管理器对象控制
- 上下文管理器对象实现了上下文管理器协议,在 with 语句执行的前后执行特定操作
执行过程
- 执行 with 后面的表达式,得到上下文管理器对象
- 执行上下文管理器对象的__enter__方法,返回值被绑定到目标变量上(as 子句)
- 不管控制流程以哪种方式退出 with 块,都会在上下文管理器对象上调用 __exit__ 方法
- with 语句的 as 子句是可选的
|
|
示例解析
- open() 函数返回的 TextIOWrapper 类实例是上下文管理器对象
- 该实例的 __enter__ 方法返回 self 赋值给 as 子句中的变量 fp
- 在 with 块的末尾,调用 TextIOWrapper.__exit__ 方法把文件关闭
- __enter__ 方法除了返回上下文管理器之外,还可能返回其他对象
2.2 上下文管理器协议
协议接口
- __enter__(self)
- 运行:with 语句开始运行时,在上下文管理器对象上调用
- 参数:
- __exit__ (self, exc_type, exc_value, traceback)
- 运行:with 语句运行结束后,在上下文管理器对象上调用,扮演着 finally 子句的角色
- 异常:返回 None,或者 True 之外的值, with 块中的任何异常都会向上冒泡
- 参数:
- exc_type:异常类(例如 ZeroDivisionError)
- exc_value 异常实例。有时会有参数传给异常构造方法,例如错误消息,这些参数可以使用 exc_value.args 获取
- traceback:traceback 对象
- 附注:
- 如果一切正常, Python 调用 __exit__ 方法时传入的参数是 None, None, None
- 在finally 块中调用 sys.exc_info(),得到的就是 __exit__ 接收的这三个参数
2.3 上下文管理器的应用
- 在 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
- 在 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
3. contextlib模块
2.1 实用工具
文档:https://docs.python.org/3/library/contextlib.html
|对象|说明|
|: —|: —|
|redirect_stdout|只需传入类似文件的对象,用于替代 sys.stdout|
|closing|如果对象提供了 close() 方法,但没有实现 __enter__/__exit__ 协议,
那么可以使用这个函数构建上下文管理器|
|suppress|构建临时忽略指定异常的上下文管理器|
|ContextDecorator|这是个基类,用于定义基于类的上下文管理器
这种上下文管理器也能用于装饰函数,在受管理的上下文中运行整个函数|
|ExitStack|这个上下文管理器能进入多个上下文管理器
with 块结束时,按照后进先出的顺序调用栈中各个上下文管理器的 __exit__ 方法
如果事先不知道 with 块要进入多少个上下文管理器,可以使用这个类
例如,同时打开任意一个文件列表中的所有文件|
2.2 @contextmanager
- 作用:
- 把简单的生成器函数变成上下文管理器,不用创建类去实现管理器协议
- 方法:
- 只需实现有一个 yield 语句的生成器,生成想让__enter__ 方法返回的值
- yield 语句的作用是把函数的定义体分成两部分
- yield 语句前面的所有代码在 with 块开始时(即解释器调用 __enter__ 方法时)执行
- yield 语句后面的代码在 with 块结束时(即调用 __exit__ 方法时)执行
- 装饰器会把函数包装成实现 __enter__ 和 __exit__ 方法的类
- 类的 __enter__ :
- 调用生成器函数,保存生成器对象(这里把它称为 gen)。
- 调用 next(gen),执行到 yield 关键字所在的位置。
- 返回 next(gen) 产出的值,以便把产出的值绑定到 with/as 语句中的目标变量上
- __exit__ 方法
- 检查有没有把异常传给 exc_type;如果有,调用 gen.throw(exception), 在生成器函数定义体中包含 yield 关键字的那一行抛出异常。
- 否则,调用 next(gen),继续执行生成器函数定义体中 yield 语句之后的代码
- 因此使用 @contextmanager 装饰器时,要把 yield 语句放在 try/finally 语句中(或者放在 with 语句中)
- 异常管理:
- 使用 @contextmanager 装饰器时,异常的处理与上下文管理器协议是相反的
- 装饰器提供的 __exit__ 方法假定发给生成器的所有异常都得到处理了,因此应该压制异常。
- 如果不想让 @contextmanager 压制异常,必须在被装饰的函数中显式重新抛出异常
- 附注:
- 在 @contextmanager 装饰器装饰的生成器中, yield 与迭代没有任何关系
- 生成器函数的作用更像是协程:执行到某一点时暂停,让客户代码运行,直到客户让协程继续做事
|
|
➊ yield ‘JABBERWOCKY’
- 这个值会绑定到 with 语句中 as 子句的目标变量上
- 执行 with 块中的代码时,这个函数会在这一点暂停
- 控制权跳出 with 块,继续执行 yield 语句之后的代码
2.3 原地文件重写
- http://www.zopatista.com/python/2013/11/26/inplace-file-rewriting/
- 使用 @contextmanager 实现的原地文件重写上下文管理器
|
|
inplace 函数
- 是个上下文管理器,为同一个文件提供了两个句柄(这个示例中的 infh 和outfh),以便同时读写同一个文件
延伸阅读
Python:
Compound statements
- 全面说明了 if、 for、 while 和 try 语句的 else 子句
- https://docs.python.org/3/reference/compound_stmts.html
上下文管理器的类型
上下文管里器
- PEP 343—The‘ with’ Statement”
- https://www.python.org/dev/peps/pep-0343/
__enter__/__exit__
- 3.3.8. With Statement Context Managers
- https://docs.python.org/3/reference/datamodel.html#with-statement-context-managers
blog:
What Makes Python Awesome?
Is it a good practice to use try-except-else in Python
- 讨论了 try/except 语句(有 else 子句,或者没有)是否符合 Python 风格
- http://stackoverflow.com/questions/16138232/is-it-a-good-practice-to-use-try-except-else-in-python
Transforming Code into Beautiful, Idiomatic Python
实用工具
The Python with Statement by Example
- http://preshing.com/20110920/the-python-with-statement-by-example/),
- 举例说明了 pycairo 图形库中的上下文管理器
书籍:
《 Python 技术手册(第 2 版)》
- 有一章是关于异常的,那一章极好地讨论了 EAFP 风格
《 Python Cookbook(第 3 版)中文版》
- 8.3 让对象支持上下文管理协议
- 9.22 以简单的方式定义上下文管理器