@contextmanager 是 Python contextlib 模块中的一个“明星”,它非常强大且实用。
在 Python 中,我们都热爱 with 语句:
with open("file.txt", "w") as f:
f.write("你好")
我们喜欢它,因为它简单、干净,并且能自动处理资源的“打开”和“关闭”。无论 with 里的代码是成功了还是出错了,open() 之后的那个“清理”工作(即关闭文件)总能被自动执行。
这种“自动设置”和“自动清理”的机制,就是由上下文管理器(Context Manager)提供的。
很长一段时间里,如果想自己做一个(比如,一个自动连接和关闭数据库的上下文管理器),必须写一个完整的类(Class),并定义 __enter__ 和 __exit__ 两个“魔法方法”。这很麻烦,也很啰嗦。
而 @contextmanager 装饰器,就是 Python 提供给我们的一个“捷径”,能用一个简单的函数来“伪装”成一个完整的上下文管理器。
核心魔法:它如何工作的?
@contextmanager 是一个装饰器,它作用于一个生成器函数(generator function)——也就是一个使用了 yield 关键字的函数
它将这个函数一分为三:
-
Setup(设置):
yield语句之前的所有代码 -
Yield(交付):
yield关键字本身。它会“暂停”函数,并把yield后面的值(如果有的话)传递给with ... as后面的变量 -
Teardown(清理):
yield语句之后的所有代码
解读一个示例代码
这几乎是 @contextmanager 最经典的教学代码:
from contextlib import contextmanager
@contextmanager
def managed_resource(name):
# --- 1. Setup (设置) ---
print(f"Setup: Acquiring {name}")
try:
# --- 2. Yield (交付) ---
yield name
# --- 3. Teardown (清理) ---
finally:
print(f"Teardown: Releasing {name}")
# --- 使用它 ---
with managed_resource("MyLock") as r:
print(f"Working with {r}")
1. 进入 with 语句
程序执行到 with managed_resource("MyLock") as r: 这一行
2. “Setup” 阶段
-
managed_resource("MyLock")函数开始执行 -
它运行
yield之前的所有代码 -
控制台打印:
Setup: Acquiring MyLock -
函数执行到
try块内
3. “Yield” 阶段(最关键!)
-
函数执行到
yield name这一行 -
“暂停”信号:
yield让managed_resource函数立即暂停 -
“交付”动作:它把
name变量(其值为"MyLock")“扔”了出去 -
as r接收:with语句中的变量r接住了扔出来的"MyLock"。所以现在r = "MyLock" -
交出控制权:
managed_resource函数暂停了,控制权被交给了with语句的内部代码块(缩进部分)
4. 执行 with 内部代码
-
程序执行
with块内部的print(f"Working with {r}") -
因为
r刚刚被赋值为"MyLock" -
控制台打印:
Working with MyLock
5. 退出 with 块
-
with内部的代码块执行完毕了 -
“唤醒”信号:
with语句通知“暂停”中的managed_resource函数:“嘿,我用完了,该你继续了!”
6. “Teardown” 阶段
-
managed_resource函数从yield暂停的地方被唤醒,继续向后执行。 -
它执行
finally:块中的代码。 -
控制台打印:
Teardown: Releasing MyLock
为什么要用 try...finally?
在示例中,yield 被 try...finally 包裹着,这是 @contextmanager 的黄金搭档。
finally 块的唯一承诺是:无论发生什么,我都一定会执行。
-
如果
with内部代码(第4步)出错了(比如抛出一个ValueError),会发生什么?-
程序会立即跳出
with内部代码。 -
managed_resource函数仍然会被“唤醒”(第5步)。 -
finally块依然会被执行!(第6步),打印Teardown: Releasing MyLock。 -
在
finally执行完毕后,那个ValueError才会继续向外抛出,让程序崩溃。
-
这就是 with 语句的核心价值:无论成功还是失败,它都能保证“清理”工作(Teardown)一定发生。这对于关闭文件、释放锁、关闭数据库连接等操作至关重要,能有效防止“资源泄露”。
两个实用的“高级”例子
@contextmanager 的美妙之处在于它的灵活性。
示例1:一个代码计时器
import time
from contextlib import contextmanager
@contextmanager
def timer():
# --- 1. Setup ---
start_time = time.time()
try:
# --- 2. Yield ---
# 我们不需要“交付”任何值
yield
# --- 3. Teardown ---
finally:
end_time = time.time()
print(f"代码块运行耗时: {end_time - start_time:.4f} 秒")
# --- 使用它 ---
with timer():
print("开始执行耗时任务...")
time.sleep(1) # 模拟一个耗时操作
print("任务结束")
输出:
开始执行耗时任务...
任务结束
代码块运行耗时: 1.0012 秒
示例2:自动的数据库连接
假设代码的很多地方都需要连接数据库。
# 假设您有一个 db_library 库
import db_library
from contextlib import contextmanager
@contextmanager
def database_connection(db_url):
print(f"正在连接到 {db_url}...")
conn = db_library.connect(db_url)
try:
# “交付”这个连接对象
yield conn
# 如果 with 块没出错,就在这里提交事务
print("事务提交!")
conn.commit()
except Exception as e:
# 如果 with 块出错了,就在这里回滚
print(f"发生错误,事务回滚: {e}")
conn.rollback()
finally:
# 无论如何,最后都要关闭连接
print("关闭数据库连接。")
conn.close()
# --- 使用它 ---
with database_connection("mysql://user:pass@host/db") as db:
db.execute("INSERT INTO users (name) VALUES ('Alice')")
db.execute("INSERT INTO users (name) VALUES ('Bob')")
# --- 再次使用,这次模拟一个错误 ---
try:
with database_connection("mysql://...") as db:
db.execute("INSERT INTO ...")
raise ValueError("模拟一个数据错误") # 模拟错误
except ValueError:
print("外部捕获到错误")
第一次使用的输出(第二次使用的内容注释掉):
正在连接到 mysql://user:pass@host/db...
事务提交!
关闭数据库连接。
第二次使用的输出(第一次使用的内容注释掉):
正在连接到 mysql://...
发生错误,事务回滚:
模拟一个数据错误
关闭数据库连接。
外部捕获到错误
总结
@contextmanager 是一个优雅的工具。它用一个简单的生成器函数,就实现了与 with 语句交互所需的所有复杂逻辑(__enter__ 和 __exit__)。
它需要思考三个关键阶段:
-
需要准备什么? (Setup -
yield之前) -
需要交给使用者什么? (Yield -
yield之后的值) -
必须清理什么? (Teardown -
finally块中)
掌握它,代码在健壮性和可读性上都会提高一个档次。
490

被折叠的 条评论
为什么被折叠?



