bigywater.tech
Python · 8

contextlib.contextmanager:一个生成器怎么变成上下文管理器

对比没有try/finally和有try/finally的@contextmanager生成器,看清理代码在with块内部抛异常时会不会被跳过。

资源泄漏
没有try/finally时清理代码被跳过
_GeneratorContextManager
@contextmanager的底层实现类型
yield=分界线
yield前对应__enter__,之后对应__exit__

做的事:用一个yield,把生成器变成上下文管理器

@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保证执行)")

验证:没有try/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 保护它。

验证:contextmanager的底层实现

被装饰后的生成器函数,调用它返回的是什么类型的对象。

结果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() 把异常扔进生成器内部)。

分步演示:yield前后对应__enter__和__exit__

每一步都是本文上面用Python 3.14跑出来的原始输出。

生成器版上下文管理器,清理代码要靠自己用try/finally保证
这一步新验证的版本

    本文用到的工具

    • 本机 Python 3.14.7
    • contextlib.contextmanager:验证有无try/finally的行为差异
    京ICP备2026067502号-1京公网安备11010502063487号