@contextmanager要求函数必须含且仅含一个yield,yield前对应__enter__、后对应__exit__;yield不可被条件包裹,其后需用try/finally保证清理,并通过sys.exc_info()判断异常。

contextlib.contextmanager 要求函数必须用 yield 分隔前后逻辑
它不是装饰任意函数都能变上下文管理器的工具——contextlib.contextmanager 本质是把一个生成器函数包装成 __enter__ 和 __exit__ 的实现。关键约束是:函数体内必须有且仅有一个 yield,且 yield 前的部分对应 __enter__,之后的部分对应 __exit__(即异常处理和清理逻辑)。
常见错误是写成普通函数、或 yield 多次、或 yield 在 if/else 分支里,这时会报 RuntimeError: generator didn't yield 或更隐蔽的 StopIteration。
-
yield必须存在,且不能被条件语句包裹(比如不能写if flag: yield) - yield 后面可以跟一个值,它将成为
with语句中as绑定的对象 - yield 之后的代码只在退出时执行,无论是否发生异常;若需区分正常退出与异常退出,要手动检查
sys.exc_info()
如何在 yield 后捕获并处理异常
默认情况下,contextlib.contextmanager 把异常原样抛出。如果你需要拦截、记录或压制异常(比如文件关闭失败不中断主流程),就得在 yield 后显式处理 sys.exc_info()。
注意:yield 本身不接收参数,但 __exit__ 阶段能拿到三元组 (exc_type, exc_value, traceback),所以得靠 sys.exc_info() 拿到当前异常上下文。
- 必须在
try...except中调用yield,否则 yield 后的代码根本不会执行 - yield 后应立即检查
sys.exc_info()[0]是否为None来判断是否有异常 - 若返回
True表示已处理异常,不再向上抛;返回None或False则继续传播
from contextlib import contextmanager
import sys
<p>@contextmanager
def suppress_file_error():
try:
yield
finally:
exc_type, exc_val, tb = sys.exc_info()
if exc_type is not None and "Permission" in str(exc_val):
print("忽略权限错误")
return True # 抑制该异常
</p>
@contextmanager 装饰的函数不能带额外参数?其实可以,但要注意调用方式
它可以带参数,但这些参数要在装饰器外层传入——也就是说,你写的不是上下文管理器本身,而是一个“返回上下文管理器的工厂函数”。这和直接定义类实现 __enter__/__exit__ 的行为一致,但容易混淆。
典型误用是写成 @contextmanager\ndef my_cm(arg): ... 然后直接 with my_cm(123): —— 这没问题;但若漏掉括号,写成 with my_cm: 就会报 TypeError: object is not callable,因为此时 my_cm 是个未调用的生成器函数对象。
- 带参的
@contextmanager函数必须被调用(加括号),返回值才是真正的上下文管理器 - 参数不能是可变默认值(如
def f(lst=[])),避免跨 with 块状态污染 - 若需复用带参逻辑,推荐封装一层工厂函数,比在 yield 前做复杂初始化更清晰
对比 class 实现,@contextmanager 在什么场景下更合适
当资源生命周期简单、无状态共享、且不需要在 __enter__ 和 __exit__ 之间传递大量中间变量时,@contextmanager 更轻量。但它无法像类那样保存实例属性,所有数据都得靠闭包或外部作用域维持。
一旦你需要多次重用同一个上下文管理器实例(比如设置超时、重试次数等配置),或者要在 __exit__ 中访问 __enter__ 返回的对象内部状态,class 方式就更可控。
- 适合一次性资源:临时目录、日志级别切换、数据库连接池借用
- 不适合跨阶段强状态依赖:比如
__enter__创建 socket,__exit__要发心跳包,就得靠类存 socket 引用 - 调试困难:yield 分割逻辑导致断点跳转不直观,异常栈也比 class 实现深一层
真正麻烦的从来不是怎么写出来,而是别人读代码时得在 yield 上下反复切视角,还容易漏掉 finally 里的清理逻辑。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











