生成器表达式包裹可迭代对象为何改变上下文清理顺序?
生成器表达式为何改变嵌套with块的异常收尾顺序?
我开发的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
补充发现
不将生成器表达式绑定到局部变量时,上下文处理行为符合预期:
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
原因解析
核心在于生成器对象的生命周期和引用计数:
在
my_code中,生成器表达式被赋值给了局部变量container_gen。当assert False抛出异常时:- 内层for循环终止,但
container_gen仍持有生成器对象的引用,导致Container.__iter__里的生成器不会被立即销毁。 - 外层
closing的with块先退出,执行reader.close(),打印CLOSE: Reader。 - 异常被外层try-except捕获,打印
handle exception。 - 直到
my_code函数执行完毕,局部变量container_gen被销毁,__iter__里的生成器才会被垃圾回收,此时才触发with块的finally逻辑,打印资源释放信息。
- 内层for循环终止,但
在
expected函数中,直接迭代container:
异常会直接传播到__iter__的with块,触发finally完成资源释放,之后才会退出外层closing的with块,顺序符合嵌套上下文的预期。在
my_code_without_local中,生成器表达式没有被绑定到变量:
for循环抛出异常后,生成器对象失去所有引用,会被立即垃圾回收,因此__iter__里的with块先完成清理,再执行外层closing的逻辑。
内容的提问来源于stack exchange,提问作者Adam Tuft
相关产品推荐
相关产品推荐

