流畅的Python 上下文管理器
上下文管理器和with块
上下文管理器对象存在的目的是管理 with 语句,就像迭代器的存在是为了管理 for 语句一样。
with 语句的目的是简化 try/finally 模式。这种模式用于保证一段代码运行完毕后执行某项操作,即便那段代码由于异常、return 语句或 sys.exit() 调用而中止,也会执行指定的操作。finally 子句中的代码通常用于释放重要的资源,或者还原临时变更的状态。
上下文管理器协议包含 __enter__ 和 __exit__ 两个方法。with 语句开始运行时,会在上下文管理器对象上调用 __enter__ 方法。with 语句运行结束后,会在上下文管理器对象上调用 __exit__ 方法,以此扮演 finally 子句的角色。
with 语句会设置一个临时的上下文,交给上下文管理器对象控制,并且负责清理上下文。这么做能避免错误并减少样板代码,因此 API 更安全,而且更易于使用。除了自动关闭文件之外,with 块还有很多用途。
最常见的例子是确保关闭文件对象:
1 | with open("a5_3_with.py") as f: |
执行 with 后面的表达式得到的结果是上下文管理器对象,不过,把值绑定到目标变量上(as 子句)是在上下文管理器对象上调用 __enter__ 方法的结果。
不管控制流程以哪种方式退出 with 块,都会在上下文管理器对象上调用 __exit__ 方法,而不是在 __enter__ 方法返回的对象上调用。
with 语句的 as 子句是可选的。对 open 函数来说,必须加上 as 子句,以便获取文件的引用。不过,有些上下文管理器会返回 None,因为没什么有用的对象能提供给用户。
上下文管理器与 __enter__ 方法返回的对象之间的区别:
1 | class LookingGlass(): |
- ❶除了self之外,Python调用
__enter__方法时不传入其他参数。 - ❷ 把原来的sys.stdout.write方法保存在一个实例属性中,供后面使用。
- ❸ 为sys.stdout.write打猴子补丁,替换成自己编写的方法。
- ❹ 返回’reversed word -> drow desrever’字符串,这样才有内容存入目标变量look。
- ❺ 这是用于取代sys.stdout.write的方法,把text参数的内容反转,然后调用原来的实现。
- ❻ 如果一切正常,Python调用
__exit__方法时传入的参数是None, None, None;如果抛出了异常,这三个参数是异常数据,如下所述。 - ❼ 重复导入模块不会消耗很多资源,因为Python会缓存导入的模块。
- ❽ 还原成原来的sys.stdout.write方法。
- ❾ 如果有异常,而且是ZeroDivisionError类型,打印一个消息……
- ❿ ……然后返回True,告诉解释器,异常已经处理了。
- ⓫ 如果
__exit__方法返回None,或者True之外的值,with块中的任何异常都会向上冒泡。
传给 __exit__ 方法的三个参数列举如下。
exc_type:异常类(例如 ZeroDivisionError);
exc_value:异常实例,有时会有参数传给异常构造方法,例如错误消息,这些参数可以使用 exc_value.args 获取;
exc_tb:traceback 对象;
在 with 块之外使用 LookingGlass 类:
1 | manager = LookingGlass() |
contextlib模块中的实用工具
@contextmanager 装饰器能减少创建上下文管理器的样板代码量,因为不用编写一个完整的类,定义 __enter__ 和 __exit__ 方法,而只需实现有一个 yield 语句的生成器,生成想让__enter__ 方法返回的值。
在使用 @contextmanager 装饰的生成器中,yield 语句的作用是把函数的定义体分成两部分:yield 语句前面的所有代码在 with 块开始时(即解释器调用 __enter__ 方法时)执行,yield 语句后面的代码在 with 块结束时(即调用 __exit__ 方法时)执行。
1 | from contextlib import contextmanager |
其中,如果在 with 块中抛出了异常,Python 解释器会将其捕获,然后在 lookingmirror 函数的 yield 表达式里再次抛出。但是,如果那里没有处理错误的代码,lookingmirror 函数会中止,永远无法恢复成原来的 sys.stdout.write 方法,导致系统处于无效状态,所以使用try来处理异常。
使用 @contextmanager 装饰器时,要把 yield 语句放在 try/finally 语句中(或者放在 with 语句中),这是无法避免的,因为我们永远不知道上下文管理器的用户会在 with 块中做什么。
contextlib.contextmanager 装饰器会把函数包装成实现 __enter__ 和 __exit__ 方法的类,通过debug可以进入源码看到类 _GeneratorContextManager 。