我有一个上下文装饰器,它完成后会产生副作用。我注意到如果我使用字典理解,就不会出现副作用。
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
在我的脚本“完成”之后发生副作用是奇怪的。这意味着如果用户使用dict理解,用户就无法使用我的contextdecorator。
我注意到这种行为在Python 3上消失了,如果我写[boom_unless_zero(i) for i in iterate_and_accumulate([0, 1])]
而不是dict理解,也不会发生这种行为。
为什么会这样?
答案 0 :(得分:6)
来自https://docs.python.org/2/reference/simple_stmts.html#the-yield-statement:
从Python 2.5版开始,现在允许在try ... finally结构的try子句中使用yield语句。如果生成器在最终确定之前没有恢复(通过达到零引用计数或通过垃圾收集),将调用generator-iterator的close()方法,允许执行任何挂起的finally子句。
换句话说,挂起finally子句直到生成器迭代器关闭时才会执行,无论是显式还是由于它被垃圾收集(引用计数或循环)。似乎Python 2列表推导和Python 3在垃圾收集迭代时效率更高。
如果你想明确关闭generator-iterator:
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()
不是必需的(实际上不存在)。