我有一个 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
这种行为让我感到非常惊讶,因为我期望命名生成器表达式
with
中的活动 container_gen
块能够以与 with
中的活动 my_code
块相同的方式进行清理,而不是那样它会一直徘徊,只有在本地名称 container_gen
超出范围时才会被清理。
您依赖于生成器中的
with
,这很尴尬,因为清理很容易像这样被延迟。具体来说,我正在讨论您作为生成器编写的 __iter__
方法。不是 genexp - 我稍后会讲到。
如果您没有完全循环
__iter__
生成器,则 with
块清理仅在生成器 close
d 时运行。像大多数人一样,您从未明确关闭过发电机。你可能从来没有想过。它唯一被关闭的时候是在生成器的 __del__
方法中,当 Python 确定生成器不可访问时,该方法就会被调用。
对于
expected
,对 __iter__
生成器的唯一引用是 for
循环对其迭代器保留的内部引用。该引用在退出 with closing(Reader()) as reader:
块之前就消失了,并且由于 CPython 具有引用计数,解释器立即检测到生成器不可访问,因此它调用 __del__
,触发您期望的清理。 (在非引用计数实现上,例如 PyPy,这种清理不会那么迅速。)
使用
my_code
,您的 __iter__
生成器可从 genexp 创建的生成器访问,而 genexp 可通过 container_gen
局部变量访问。该局部变量可以通过异常回溯访问,因此直到 except
块完成并且异常对象消失之前它不会被清除。 (即使您的 except
块没有 as
子句也是如此 - 在 sys.exc_info()
块期间仍然可以通过
except
获得异常及其回溯。)
您的发电机的使用寿命比您预期的要长得多。由于您期望的清理仅由生成器的
__del__
方法触发,因此清理也会延迟同样长的时间。