Python中的with语句可谓是相当的好用了,省去了try...finally...的复杂写法,浅谈 Python 的 with 语句 一文写的已经非常详细里,解决了使用过程中的两点疑问。文中的代码清单也主要摘自这篇文章。

清单1: with语句执行过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
context_manager = context_expression
exit = type(context_manager).__exit__
value = type(context_manager).__enter__(context_manager)
exc = True # True 表示正常执行,即便有异常也忽略;False 表示重新抛出异常,需要对异常进行处理
try:
try:
target = value # 如果使用了 as 子句
with-body # 执行 with-body
except:
# 执行过程中有异常发生
exc = False
# 如果 __exit__ 返回 True,则异常被忽略;如果返回 False,则重新抛出异常
# 由外层代码对异常进行处理
if not exit(context_manager, *sys.exc_info()):
raise
finally:
# 正常退出,或者通过 statement-body 中的 break/continue/return 语句退出
# 或者忽略异常退出
if exc:
exit(context_manager, None, None, None)
# 缺省返回 None,None 在布尔上下文中看做是 False

从上边的逻辑可以看出,1.如果with语句块发生异常,那么执行except中的流程释放资源,并根据exit()函数返回值决定是否向上抛出异常;2.如果执行过程无异常,那么走finally中的逻辑释放资源。

1. 还用不用处理异常?

异常是必须处理的, with语句解决的只是保证了finally中的语句的执行,在with语句块内发生了异常而有没有处理,那么会直接执行with所代替的finally中的语句,并抛出异常给外层上下文,和正常情况下的异常处理流程是一样的。

2. 如果__exit__内部异常?

引用文章浅谈 Python 的 with 语句 中的解释:

退出与上下文管理器相关的运行时上下文,返回一个布尔值表示是否对发生的异常进行处理。参数表示引起退出操作的异常,如果退出时没有发生异常,则3个参数都为None。如果发生异常,返回
True 表示不处理异常,否则会在退出该方法后重新抛出异常以由 with 语句之外的代码逻辑进行处理。如果该方法内部产生异常,则会取代由 statement-body 中语句产生的异常。要处理异常时,不要显示重新抛出异常,即不能重新抛出通过参数传递进来的异常,只需要将返回值设置为 False 就可以了。之后,上下文管理代码会检测是否 exit() 失败来处理异常

通过例子来看一下:

  • with语句块异常,但正确释放资源

清单2: 自定义支持 with 语句的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class DummyResource:
def __init__(self, tag):
self.tag = tag
print 'Resource [%s]' % tag
def __enter__(self):
print '[Enter %s]: Allocate resource.' % self.tag
return self # 可以返回不同的对象
def __exit__(self, exc_type, exc_value, exc_tb):
print '[Exit %s]: Free resource.' % self.tag
if exc_tb is None:
print '[Exit %s]: Exited without exception.' % self.tag
else:
print '[Exit %s]: Exited with exception raised.' % self.tag
return False # 可以省略,缺省的None也是被看做是False

清单3: with语句块引起异常

1
2
3
4
5
6
7
try:
with DummyResource('With-Exception'):
print '[with-body] Run with exception.'
raise Exception('with-block exception')
print '[with-body] Run with exception. Failed to finish statement-body!'
except Exception as e:
print e

执行上边的代码,可以看到打印的信息,释放完资源后,异常抛出给with外层进行处理,如果外层没有try语句的话,在本例中会由系统打印调用栈并退出。
如果__exit__返回True,那么打印信息的最后一行with-block exception将不会被打印,因为with语句释放完资源后,把这个异常“压”下来了。
清单4: 正常释放资源,抛出原有异常

1
2
3
4
5
[Enter With-Exception]: Allocate resource.
[with-body] Run with exception.
[Exit With-Exception]: Free resource.
[Exit With-Exception]: Exited with exception raised.
with-block exception

稍微对上边的代码修改一下,在__exit__函数中重新抛出一个异常,如清单5,重新执行清单3的调用,得到清单6的结果。
清单5:__exit__抛出异常

1
2
3
4
5
6
7
def __exit__(self, exc_type, exc_value, exc_tb):
print '[Exit %s]: Free resource.' % self.tag
if exc_tb is None:
print '[Exit %s]: Exited without exception.' % self.tag
else:
print '[Exit %s]: Exited with exception raised.' % self.tag
raise Exception('new exception in __exit__')

清单6: __exit__抛出新异常执行结果

1
2
3
4
5
6
Resource [With-Exception]
[Enter With-Exception]: Allocate resource.
[with-body] Run with exception.
[Exit With-Exception]: Free resource.
[Exit With-Exception]: Exited with exception raised.
__exit__ exception.

可以看出,如果__exit__重新抛出了一个异常,就会覆盖掉原有异常,如果资源正常释放,只需要返回False或者True来表示是否将with语句的异常抛给外层调用。

3.其他使用

使用contextmanager装饰器、nested函数、closing上下文管理器等语法可以参考
浅谈 Python 的 with 语句】或者 【contextlib — Utilities for with-statement contexts】等文章,不再赘述。

参考

  1. 浅谈 Python 的 with 语句
  2. contextlib — Utilities for with-statement contexts
  3. PEP 343 – The “with” Statement