提问人:Adam Tuft 提问时间:10/19/2023 最后编辑:Adam Tuft 更新时间:10/20/2023 访问量:68
将可迭代对象包含在生成器表达式中会更改上下文的清理顺序
Enclosing an iterable in a generator expression changes the order that contexts are cleaned up
问:
我有一个 Python 应用程序,它有两个嵌套块,其中一个发生在对象的方法中。我观察到,当引发异常时,将可迭代对象包装在生成器表达式中会改变这些块的最终顺序,我对此感到非常惊讶。with
__iter__
with
我正在使用 python 3.11.5,但在 3.8.17 中观察到相同的行为。
下面是一个演示器,它再现了观察到的行为以及预期的行为:
from contextlib import closing, contextmanager
class Reader:
def close(self):
print(f"CLOSE: {self.__class__.__name__}")
@contextmanager
def get_resource(self):
try:
yield 42
finally:
print(f"CLOSE: {self.__class__.__name__} releases resource")
class Container:
def __init__(self, reader: Reader):
self._reader = reader
def __iter__(self):
with self._reader.get_resource() as resource:
for x in range(10):
yield x
def my_code():
with closing(Reader()) as reader:
container = Container(reader)
container_gen = (x for x in container)
for thing in container_gen:
assert False
def expected():
with closing(Reader()) as reader:
container = Container(reader)
for thing in container:
assert False
print(">>> What my code does:")
try:
my_code()
except AssertionError:
print("handle exception")
print()
print(">>> The context handling I expected:")
try:
expected()
except AssertionError:
print("handle exception")
这是我从这段代码中得到的输出:
>>> What my code does:
CLOSE: Reader
handle exception
CLOSE: Reader releases resource
>>> The context handling I expected:
CLOSE: Reader releases resource
CLOSE: Reader
handle exception
似乎将 包装在生成器表达式中会导致在异常处理完成后清理其方法中的块,尽管它嵌套在另一个块中,该块在引发异常时被清理。Container
with
__iter__
with
为什么将可迭代对象包装在生成器表达式中会改变其方法中块在异常处理期间的最终确定方式?with
__iter__
编辑:
多亏了@user2357112的回答,我看到省略绑定到生成器表达式的局部变量可以得到我期望的上下文处理:
def my_code_without_local():
with closing(Reader()) as reader:
container = Container(reader)
for thing in (x for x in container):
assert False
这将给出以下输出:
CLOSE: Reader releases resource
CLOSE: Reader
handle exception
这种行为对我来说真的很惊讶,因为我期望命名生成器表达式中的活动 -block 以与 中的活动 -block 相同的方式进行清理,而不是说它会徘徊,只有在本地名称超出范围时才会被清理。with
container_gen
with
my_code
container_gen
答:
你依赖于一个生成器,这些很尴尬,因为清理很容易像这样延迟。具体来说,我说的是你的方法,你把它写成一个生成器。不是 genexp - 我稍后会谈到。with
__iter__
如果未完全循环生成器,则块清理仅在生成器为 d 时运行。像大多数人一样,您从未明确关闭过生成器。你可能从未想过。它唯一被关闭的时间是在生成器的方法中,当 Python 确定生成器无法访问时,就会调用该方法。__iter__
with
close
__del__
使用 时,对生成器的唯一引用是循环对其迭代器的内部引用。该引用在块退出之前死亡,并且由于 CPython 具有引用计数,因此解释器会立即检测到生成器无法访问,因此它会调用 ,从而触发您预期的清理。(在非引用计数实现(如 PyPy)上,此清理不会那么及时。expected
__iter__
for
with closing(Reader()) as reader:
__del__
使用 ,您的生成器可以从 genexp 创建的生成器访问,该生成器可通过局部变量访问。该局部变量可通过异常回溯访问,因此在块完成且异常对象死亡之前,它不会被清除。(即使您的块没有子句,也是如此 - 在块期间,异常及其回溯仍可通过 sys.exc_info()
获得。my_code
__iter__
container_gen
except
except
as
except
您的发电机最终的寿命比您预期的要长得多。由于您期望的清理仅由生成器的方法触发,因此清理延迟的时间也一样长。__del__
评论
with
with
my_code
with
with
with
container_gen
with
yield
yield
yield
expected
上一个:如何捕获特定于包的异常?
评论