结合yield与抑制异常的ContextDecorator时为何会触发GeneratorExit?
咱们先拆解一下你遇到的问题,核心是外部迭代时抛出异常会触发生成器强制关闭,再加上你的上下文管理器不当处理了GeneratorExit,才导致了这个连锁问题。
问题根源拆解
外部异常会触发生成器的
.close()
当你在for i, raising in enumerate(MyCD.yield_each_item_wrapped(raising_flags))循环内部抛出Exception("my error")时,Python会立即终止当前迭代流程,自动调用生成器的.close()方法。这会让生成器在当前挂起的yield item位置抛出GeneratorExit异常——这是Python生成器的内置机制,用来强制生成器清理并退出。你的ContextDecorator误抑制了
GeneratorExit
你的MyCD.__exit__方法返回True,试图抑制所有异常(包括GeneratorExit)。但Python对GeneratorExit有特殊规则:生成器收到这个异常时,必须无条件退出,不能继续产生值。如果你的上下文管理器强行抑制它,生成器会认为自己被非法阻止退出,进而抛出RuntimeError: generator ignored GeneratorExit。为什么第一个示例能正常工作?
你写的yield2函数里,异常是在with块内部处理的,外部循环根本没有抛出异常,所以生成器从来不会被调用.close(),自然不会触发GeneratorExit,能正常迭代后续元素。
解决方案
针对你的需求(继续处理剩余元素),给你几个可行的方案:
方案1:把异常逻辑移到生成器内部处理
最稳妥的方式是让生成器自己处理每个item的异常,只返回成功处理的结果,这样外部循环不需要抛出异常,也就不会触发生成器关闭:
import traceback from contextlib import ContextDecorator class MyCD(ContextDecorator): @classmethod def yield_each_item_wrapped(cls, items, **kwargs): for i, item in enumerate(items): print(f"yield_each_item_wrapped {i=}") print(f"yield_each_item_wrapped {item=}") try: with cls(**kwargs): if item: raise Exception("my error") yield i # 只yield成功处理的索引 except Exception: traceback.print_exc() # 内部消化异常,不传播到外部循环 def __enter__(self): print("__enter__") def __exit__(self, exc_type, exc, exc_tb): print("__exit__") if exc: traceback.print_exc() return True # 只抑制业务异常,GeneratorExit不会走到这里 if __name__ == "__main__": raising_flags = [True, False, False, True] expected = [1, 2] actual = list(MyCD.yield_each_item_wrapped(raising_flags)) assert expected == actual, (expected, actual)
方案2:在__exit__中区分GeneratorExit,不抑制它
如果你一定要保留外部抛出异常的逻辑,可以修改__exit__,让GeneratorExit正常传播,这样生成器能正确退出(但后续元素会停止处理,适合不需要继续迭代的场景):
def __exit__(self, exc_type, exc, exc_tb): print("__exit__") if exc: traceback.print_exc() # 只抑制业务异常,GeneratorExit让它正常传播 return exc_type is not GeneratorExit
方案3:外部循环捕获异常,避免触发生成器关闭
如果必须在外部处理业务逻辑,可以在外部捕获异常,不让异常终止整个循环,这样生成器不会被强制关闭:
if __name__ == "__main__": raising_flags = [True, False, False, True] expected = [1, 2] actual = [] gen = MyCD.yield_each_item_wrapped(raising_flags) for i, raising in enumerate(gen): try: if raising: raise Exception("my error") actual.append(i) except Exception: traceback.print_exc() # 外部捕获异常,不终止生成器迭代 assert expected == actual, (expected, actual)
总结
核心问题就是外部异常触发了生成器的强制关闭机制,而你的上下文管理器错误地抑制了GeneratorExit这个特殊异常。解决的关键要么避免触发生成器关闭,要么遵循Python规则不抑制GeneratorExit。
备注:内容来源于stack exchange,提问作者zsepi

