简体   繁体   English

为什么这个上下文管理器与dict理解有不同的表现?

[英]Why does this contextmanager behave differently with dict comprehensions?

I have a context decorator that has side effects when it's done. 我有一个上下文装饰器,它完成后会产生副作用。 I've noticed that the side effects don't occur if I use a dict comprehension. 我注意到如果我使用字典理解,就不会出现副作用。

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!')

Output: 输出:

$ 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

It's bizarre that the side effect occurs after my script is 'finished'. 在我的脚本“完成”之后发生副作用是奇怪的。 It means users can't use my contextdecorator if they're using dict comprehensions. 这意味着如果用户使用dict理解,则用户无法使用我的contextdecorator。

I've noticed that this behaviour disappears on Python 3, and the behaviour also doesn't occur if I write [boom_unless_zero(i) for i in iterate_and_accumulate([0, 1])] instead of the dict comprehension. 我注意到这种行为在Python 3上消失了,如果我[boom_unless_zero(i) for i in iterate_and_accumulate([0, 1])]编写[boom_unless_zero(i) for i in iterate_and_accumulate([0, 1])]而不是dict理解,那么行为也不会发生。

Why does this occur? 为什么会这样?

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

As of Python version 2.5, the yield statement is now allowed in the try clause of a try ... finally construct. 从Python 2.5版开始,现在允许在try ... finally结构的try子句中使用yield语句。 If the generator is not resumed before it is finalized (by reaching a zero reference count or by being garbage collected), the generator-iterator's close() method will be called, allowing any pending finally clauses to execute. 如果生成器在最终确定之前没有恢复(通过达到零引用计数或通过垃圾收集),将调用generator-iterator的close()方法,允许执行任何挂起的finally子句。

In other words, pending finally clauses will not execute until the generator-iterator is closed, either explicitly or as a result of it being garbage-collected (refcount or cyclic). 换句话说,挂起finally子句直到生成器迭代器被关闭时才会执行,无论是显式还是由于它被垃圾收集(引用计数或循环)。 It seems that Python 2 list comprehensions and Python 3 are more efficient at garbage collecting the iterable. 似乎Python 2列表推导和Python 3在垃圾收集迭代方面更有效。

If you want to be explicit about closing the 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)

I had a look at the underlying issue; 我看了一下潜在的问题; it seems that the problem is that the generator-iterator is held by the exception traceback state, so another workaround is to call sys.exc_clear() : 似乎问题是生成器迭代器由异常回溯状态保持,所以另一个解决方法是调用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)

In Python 3, the lexical exception handler system ( http://bugs.python.org/issue3021 ) means that the exception state is cleared on exit from the handler block, so sys.exc_clear() is not necessary (and indeed is not present). 在Python 3中,词法异常处理程序系统( http://bugs.python.org/issue3021 )意味着异常状态在从处理程序块退出时被清除,因此sys.exc_clear()不是必需的(事实上并非如此)当下)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM