with 语句根本听不懂函数的语言,它只听得懂"类"的语言(即 __enter__ 和 __exit__)。
@contextlib.contextmanager 的作用就是把你的生成器函数"包装"成一个 with 语句能听懂的类。
下面我为你拆解它的核心机制:
1. 为什么不能直接用?
如果你直接把一个包含 try...finally 和 yield 的普通生成器函数放到 with 后面,程序会直接报错。
python
def my_gen():
try:
yield 1
finally:
print("清理工作")
# ❌ 这样写会报错!
# AttributeError: __enter__
# with my_gen() as x:
# pass
原因: with 语句期望后面的对象必须有 __enter__ 和 __exit__ 方法。普通的生成器函数返回的是一个生成器对象,它没有这两个方法。
2. @contextlib.contextmanager 到底做了什么?
当你给函数加上这个装饰器时,Python 在背后做了一个"偷天换日"的操作:
- 它捕获了你的生成器函数。
- 它返回了一个新的对象 (Helper Class),这个对象内部实现了
__enter__和__exit__。 - 它在
__exit__方法里,帮你去手动触发 了生成器的后续代码(也就是你的finally部分)。
3. 执行流程图解(核心原理)
为了让你明白 finally 是怎么被触发的,请看这个流程:
-
with开始 -> 调用装饰器生成的对象的__enter__。 -
__enter__内部 -> 调用next(your_generator)。- 生成器开始运行 -> 遇到
yield暂停。 __enter__拿到yield出来的值,并返回给as后的变量。
- 生成器开始运行 -> 遇到
-
用户代码块 (
with内部) -> 执行你的业务逻辑。 -
with结束 -> 自动调用装饰器生成的对象的__exit__。 -
__exit__内部 -> 关键点来了!- 它会再次调用
next(your_generator)(或者throw如果有异常)。 - 这导致生成器从
yield后面继续运行。 - 因此,你的
finally代码块才得以执行。
- 它会再次调用
4. 自己模拟一个 contextmanager
为了证明这一点,我们可以写一个简化版的类,来实现和 @contextlib.contextmanager 一样的功能。看完这个你就彻底懂了:
python
class MyContextManagerWrapper:
def __init__(self, generator_func, *args, **kwargs):
# 1. 初始化时,创建一个生成器对象
self.gen = generator_func(*args, **kwargs)
def __enter__(self):
# 2. 进入 with 时,运行生成器直到遇到 yield
try:
return next(self.gen)
except StopIteration:
raise RuntimeError("生成器没有 yield 任何值!")
def __exit__(self, exc_type, exc_value, traceback):
# 3. 退出 with 时,核心逻辑在这里!
if exc_type is None:
try:
# 如果没有异常,让生成器继续运行(执行 yield 后面的代码,即 finally)
next(self.gen)
except StopIteration:
# 生成器正常结束是预期的
return False
else:
try:
# 如果有异常,把异常扔回给生成器
self.gen.throw(exc_type, exc_value, traceback)
except (StopIteration, exc_type):
return False
except Exception:
# 处理生成器内部的新异常
return False
# --- 测试 ---
def simple_func():
print("1. setup")
try:
yield "资源"
finally:
print("3. teardown (finally 执行了)")
# 使用我们要包装的类
with MyContextManagerWrapper(simple_func) as res:
print(f"2. inside with: {res}")
输出:
Plaintext
markdown
1. setup
2. inside with: 资源
3. teardown (finally 执行了)
总结
@contextlib.contextmanager 不是仅仅为了执行 finally,它的作用是:
- 桥接 (Bridge): 把"生成器"转换成符合"上下文管理器协议"的对象。
- 驱动 (Driver): 它负责在
__enter__时启动生成器,在__exit__时恢复生成器的执行。
正是因为它在 __exit__ 里手动帮你"踹"了生成器一脚(调用了 next()),生成器才得以从 yield 醒来并往下走,从而执行到了你的 finally 代码块。