为什么这个上下文管理器在字典推导中行为不同?
我有一个上下文装饰器,它在完成时会产生一些副作用。我发现如果我使用字典推导式,这些副作用就不会发生。
from contextlib import contextmanager
import traceback
import sys
accumulated = []
@contextmanager
def accumulate(s):
try:
yield
finally:
print("Appending %r to accumulated" % s)
accumulated.append(s)
def iterate_and_accumulate(iterable):
for item in iterable:
with accumulate(item):
yield item
def boom_unless_zero(i):
if i > 0:
raise RuntimeError("Boom!")
try:
{i: boom_unless_zero(i) for i in iterate_and_accumulate([0, 1])}
except:
traceback.print_exc()
print(accumulated)
print('\n=====\n')
try:
{i: boom_unless_zero(i) for i in iterate_and_accumulate([0, 1])}
except:
traceback.print_exc()
print(accumulated)
print('Finished!')
输出:
$ python2 boom3.py
Appending 0 to accumulated
Traceback (most recent call last):
File "boom3.py", line 25, in <module>
{i: boom_unless_zero(i) for i in iterate_and_accumulate([0, 1])}
File "boom3.py", line 25, in <dictcomp>
{i: boom_unless_zero(i) for i in iterate_and_accumulate([0, 1])}
File "boom3.py", line 22, in boom_unless_zero
raise RuntimeError("Boom!")
RuntimeError: Boom!
[0]
=====
Appending 0 to accumulated
Appending 1 to accumulated
Traceback (most recent call last):
File "boom3.py", line 34, in <module>
{i: boom_unless_zero(i) for i in iterate_and_accumulate([0, 1])}
File "boom3.py", line 34, in <dictcomp>
{i: boom_unless_zero(i) for i in iterate_and_accumulate([0, 1])}
File "boom3.py", line 22, in boom_unless_zero
raise RuntimeError("Boom!")
RuntimeError: Boom!
[0, 0, 1]
Finished!
Appending 1 to accumulated
很奇怪的是,这些副作用发生在我的脚本“完成”之后。这意味着如果用户使用字典推导式,就不能使用我的上下文装饰器。
我注意到在Python 3中,这种行为消失了,而且如果我写成[boom_unless_zero(i) for i in iterate_and_accumulate([0, 1])]
,而不是使用字典推导式,副作用也不会出现。
这是为什么呢?
1 个回答
6
根据 这个链接 的内容:
从Python 2.5版本开始,yield语句可以在try ... finally结构的try部分使用。如果生成器在结束之前没有被重新启动(比如说引用计数变为零或者被垃圾回收),那么生成器迭代器的close()方法会被调用,这样可以执行任何待处理的finally部分。
换句话说,待处理的finally部分不会执行,直到生成器迭代器被关闭,不管是手动关闭还是因为垃圾回收而关闭(比如引用计数或循环引用)。看起来Python 2的列表推导和Python 3在垃圾回收可迭代对象方面更有效率。
如果你想明确地关闭生成器迭代器,可以这样做:
from contextlib import closing
try:
with closing(iter(iterate_and_accumulate(a))) as it:
{i: boom_unless_zero(i) for i in it}
except:
traceback.print_exc()
print(accumulated)
我查看了底层问题,似乎问题在于生成器迭代器被异常追踪状态所持有,所以另一种解决方法是调用 sys.exc_clear()
:
import sys
try:
{i: boom_unless_zero(i) for i in iterate_and_accumulate(a)}
except:
traceback.print_exc()
try:
sys.exc_clear()
except AttributeError:
pass
print(accumulated)
在Python 3中,词法异常处理系统(http://bugs.python.org/issue3021)意味着在退出处理块时,异常状态会被清除,所以 sys.exc_clear()
就不需要了(实际上也没有这个函数)。