【问题标题】:Why does this contextmanager behave differently with dict comprehensions?为什么这个 contextmanager 的行为与 dict 理解不同?
【发布时间】:2014-08-21 20:16:08
【问题描述】:

我有一个上下文装饰器,它在完成后会产生副作用。我注意到,如果我使用 dict 理解,则不会出现副作用。

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])] 而不是 dict 理解,这种行为也不会发生。

为什么会这样?

【问题讨论】:

    标签: python python-2.7 exception generator


    【解决方案1】:

    来自https://docs.python.org/2/reference/simple_stmts.html#the-yield-statement

    从 Python 2.5 版开始,现在允许在 try ... finally 构造的 try 子句中使用 yield 语句。如果生成器在最终确定之前没有恢复(通过达到零引用计数或被垃圾收集),则将调用生成器迭代器的 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() 不是必需的(实际上不存在)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-09-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-09-22
      • 2019-08-19
      • 2014-08-08
      • 1970-01-01
      相关资源
      最近更新 更多