为什么这个上下文管理器在字典推导中行为不同?

4 投票
1 回答
593 浏览
提问于 2025-04-18 11:42

我有一个上下文装饰器,它在完成时会产生一些副作用。我发现如果我使用字典推导式,这些副作用就不会发生。

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() 就不需要了(实际上也没有这个函数)。

撰写回答