对比没有try/finally和有try/finally的@contextmanager生成器,看清理代码在with块内部抛异常时会不会被跳过。
@contextlib.contextmanager把"yield之前"当__enter__,"yield之后"当__exit__。
代码:两个版本# 版本1:没有try/finally @contextlib.contextmanager def bad_resource(name): print(f"[{name}] 打开资源") yield name print(f"[{name}] 关闭资源") # 如果yield之后抛异常,这行会被跳过 # 版本2:有try/finally @contextlib.contextmanager def good_resource(name): print(f"[{name}] 打开资源") try: yield name finally: print(f"[{name}] 关闭资源(finally保证执行)")
with块内部抛出异常,看两个版本各自的表现。
结果:bad_resource(没有try/finally)[bad] 打开资源 捕获到异常了,但"关闭资源"那行没有打印! 结果:good_resource(有try/finally)[good] 打开资源 [good] 关闭资源(finally保证执行) 捕获到异常了,上面关闭资源那行打印了
对比很清楚:bad_resource 里,yield 之后的清理代码 print("关闭资源") 没有执行到——因为 with 块内部抛出的异常,是在 yield 这一行"重新抛出"到生成器函数内部的,如果生成器函数自己没有 try/finally 兜底,这个异常会直接往上冒,跳过yield之后所有还没执行的代码。good_resource 用 try/finally 包住了 yield,保证了不管yield那里发生什么,finally 块都会执行。
@contextlib.contextmanager 写资源管理代码时最容易踩的坑:写生成器函数时,必须把yield放进try/finally里,不然一旦with块内部抛异常,清理代码就会被跳过——这跟手写 __exit__ 类不一样,手写的类里 __exit__ 方法本身作为一个独立的方法,总会被调用,但生成器函数的"后半段"能不能执行到,完全取决于你有没有用 try/finally 保护它。
被装饰后的生成器函数,调用它返回的是什么类型的对象。
结果cm = good_resource("check") type(cm) → :contextlib._GeneratorContextManager hasattr(cm, '__enter__') → :True hasattr(cm, '__exit__') → :True
验证了 @contextlib.contextmanager 底层做的事:把普通的生成器函数,包装成一个实现了 __enter__/__exit__ 的 _GeneratorContextManager 对象——__enter__ 做的是调用生成器直到第一个 yield(拿到yield的值作为返回值),__exit__ 做的是把生成器"送回去"继续执行(如果with块有异常,就用 throw() 把异常扔进生成器内部)。
每一步都是本文上面用Python 3.14跑出来的原始输出。
contextlib.contextmanager:验证有无try/finally的行为差异