15.1 先做这个,再做那个:if 语句之外的 else 块
for/else、while/else 和 try/else 的语义关系紧密,不过与 if/else 差别很大:
- for: 仅当 for 循环运行完毕时(即 for 循环没有被 break 语句中止)才运行 else 块。
- while:仅当 while 循环因为条件为假值而退出时(即 while 循环没有被 break 语句中止)才运行 else 块。
- try:仅当 try 块中没有异常抛出时才运行 else 块。(注意,else 中抛出的异常不会由前面的 except 子句处理)
在所有情况下,如果异常或者 return、break 或 continue 语句导致 控制权跳到了复合语句的主块之外,else 子句也会被跳过。
15.2 上下文管理器和 with 块
上下文管理器对象存在的目的是管理 with 语句,就像迭代器的存在是为了管理 for 语句一样。
上下文管理器协议包含 __enter__
和 __exit__
两个方法。with 语句开始运行时,会在上下文管理器对象上调用 __enter__
方法。with 语句运行结束后,会在上下文管理器对象上调用 __exit__
方法,以此扮演 finally 子句的角色。
不管控制流程以哪种方式退出 with 块,都会在上下文管理器对象上调用 __exit__
方法,而不是在 __enter__
方法返回的对象上调用。
下面的例子使用一个精心制作的上下文管理器执行操作,以此强调上下文管理器与 __enter__
方法返回的对象之间的区别:
1 | In [5]: class LookingGlass: |
- 如果一切正常,Python 调用
__exit__
方法时传入的参数是None
,None
,None
;如果抛出了异常,这三个参数是异常数据。exc_type
:异常类。exc_value
:异常实例。有时会有参数传给异常构造方法,例如错误消息,这些参数可以使用exc_value.args
获取。traceback
:traceback 对象。
- 如果
__exit__
方法返回None
,或者True
之外的值,with 块中的任何异常都会向上冒泡。
上下文管理器的具体工作方式参见下面的例子。在这个示例中,我们在 with 块之外使用 LookingGlass 类,因此可以手动调用 __enter__
和 __exit__
方法:
1 | In [8]: manager = LookingGlass() |
15.3 contextlib 模块中的实用工具
closing
如果对象提供了 close()
方法,但没有实现 __enter__
/__exit__
协议,那么可以使用这个函数构建上下文管理器。
1 | from contextlib import closing |
suppress
构建临时忽略指定异常的上下文管理器。
1 | from contextlib import suppress |
上面的代码和下面的代码是等价的:
1 | try: |
@contextmanager
这个装饰器把简单的生成器函数变成上下文管理器,这样就不用创建类去实现管理器协议了。
1 | from contextlib import contextmanager |
ContextDecorator
这是个基类,用于定义基于类的上下文管理器。这种上下文管理器也能用于装饰函数,在受管理的上下文中运行整个函数。
1 | from contextlib import ContextDecorator |
ExitStack
这个上下文管理器能进入多个上下文管理器。with 块结束时,ExitStack 按照后进先出的顺序调用栈中各个上下文管理器的 __exit__
方法。如果事先不知道 with 块要进入多少个上下文管理器,可以使用这个类。例如,同时打开任意一个文件列表中的所有文件。
1 | with ExitStack() as stack: |
15.4 使用 @contextmanager
其实,contextlib.contextmanager
装饰器会把函数包装成实现 __enter__
和 __exit__
方法的类。
这个类的 __enter__
方法有如下作用:
- 调用生成器函数,保存生成器对象(这里把它称为 gen)。
- 调用
next(gen)
,执行到 yield 关键字所在的位置。 - 返回
next(gen)
产出的值,以便把产出的值绑定到 with/as 语句中的目标变量上。
with 块终止时,__exit__
方法会做一下几件事:
- 检查有没有把异常传给
exc_type
;如果有,调用gen.throw(exception)
,在生成器函数定义体中包含 yield 关键字的那一行抛出异常。 - 否则,用
next(gen)
,继续执行生成器函数定义体中 yield 语句之后的代码。
使用 @contextmanager 装饰器时,要把 yield 语句放在 try/finally 语句中(或者放在 with 语句中),这是无法避免的,因为我们永远不知道上下文管理器的用户会在 with 块中做什么。