在之前的文章中我们简单的介绍过contextmanager装饰器,有朋友私信说内容太过于简洁了,今天我们就来详细的介绍一下它。
什么是@contextmanager?它是 Python 标准库 contextlib 模块中的一个装饰器,核心作用是:让生成器函数直接变成上下文管理器,无需手动编写包含 __enter__ 和 __exit__ 方法的类。它的核心价值的是“简化资源管理”——无论是打开关闭文件、数据库连接,还是执行 setup/teardown 操作、临时修改状态,都能通过简洁的代码实现。
@contextmanager 几乎能覆盖所有上下文管理器的使用场景,尤其是以下5种高频场景,用它能大幅提升开发效率:
资源管理:打开/关闭文件、数据库连接、网络套接字,或在 API 调用前后自动执行登录/登出操作;
setup/teardown 操作:搭建测试夹具(比如 pytest 测试前准备、测试后清理)、临时配置初始化;
临时状态切换:临时切换工作目录、修改环境变量、屏蔽控制台输出;
锁与同步:自动获取和释放锁,避免死锁;
事务管理:数据库事务的开启、提交/回滚,确保数据一致性。
接下来我们结合代码来看看基础用法和传统类实现的对比,例如要实现一个API会话管理(自动登录、登出),传统方式需要写一个类,重写__enter__和__exit__
import requests# 传统类实现上下文管理器class SessionManager: def __init__(self, base_url, token): self.base_url = base_url self.token = token self.session = None # 进入with块时执行(初始化资源) def __enter__(self): self.session = requests.Session() self.session.headers.update({"Authorization": f"Bearer {self.token}"}) response = self.session.post(f"{self.base_url}/login") response.raise_for_status() return self.session # 退出with块时执行(清理资源) def __exit__(self, exc_type, exc_val, exc_tb): self.session.post(f"{self.base_url}/logout")# 使用with SessionManager("https://my-base-url.com", "my-fake-token") as session: refresh_reports(session) download_reports(session)
我们来看看@contextmanager装饰器是如何搞定上面的问题的:
from contextlib import contextmanagerimport requests# 用生成器函数 + 装饰器实现上下文管理器@contextmanagerdef session_manager(base_url, token): try: # 1. 进入with块时执行(对应__enter__) session = requests.Session() session.headers.update({"Authorization": f"Bearer {token}"}) response = session.post(f"{base_url}/login") response.raise_for_status() yield session # 返回给with...as的变量(相当于__enter__的return) finally: # 2. 退出with块时执行(对应__exit__),无论是否报错都会执行 session.post(f"{base_url}/logout")# 使用方式完全一致!with session_manager("https://my-base-url.com", "my-fake-token") as session: refresh_reports(session) download_reports(session) email_reports(session)
它的核心原理非常简单,只需要记住3步:1.yield之前的代码,进入with块时执行,负责初始化资源;2.yield语句,将后面的变量返回给with...as,作为上下文内使用的对象;3.yield之后的代码,退出with块时执行,负责清理资源。
我们来看个常见的使用常见,代码执行计时:
from contextlib import contextmanagerimport time@contextmanagerdef timer(label): start = time.time() # 进入时记录开始时间 try: yield # 不返回值,仅用于控制上下文 finally: end = time.time() # 退出时计算耗时 print(f"{label}: {end - start:.4f} seconds")# 使用with timer("数据处理耗时"): # 模拟耗时操作 time.sleep(0.5) do_something()# 输出:数据处理耗时: 0.5001 seconds
还有数据库事务管理场景:
from contextlib import contextmanagerimport sqlite3@contextmanagerdef db_transaction(db_path, commit=False): # 1. 初始化数据库连接和游标 conn = sqlite3.connect(db_path) cursor = conn.cursor() try: yield cursor # 返回游标,供with块内执行SQL conn.commit() # 无异常则提交事务 except Exception as e: conn.rollback() # 有异常则回滚 raise # 重新抛出异常,让外部捕获(不隐藏错误) finally: conn.close() # 无论是否报错,都关闭连接# 使用:执行两条插入语句,要么都成功,要么都失败with db_transaction("mydb.db") as cursor: cursor.execute("INSERT INTO users (name) VALUES (?)", ("Alice",)) cursor.execute("INSERT INTO users (name) VALUES (?)", ("Bob",))
@contextmanager 虽然简洁,但有几个容易踩坑的细节,尤其是新手,一定要注意,否则会出现资源泄漏、异常隐藏等问题。
如果清理代码(比如关闭连接、登出)不放在 finally 块中,一旦 with 块内抛出异常,清理代码会无法执行,导致资源泄漏。
from contextlib import contextmanager# 错误写法:清理代码不在finally,可能不执行@contextmanagerdef bad_context(): resource = acquire_resource() yield resource release_resource(resource) # 若yield后报错,这里不会执行!# 正确写法:清理代码放在finally,必执行@contextmanagerdef good_context(): resource = acquire_resource() try: yield resource finally: release_resource(resource) # 无论是否报错,都执行
一个上下文管理器只能有一个 yield,生成器函数中只能有一个 yield 语句,多个 yield 只会执行第一个,后面的都会被忽略。
from contextlib import contextmanager@contextmanagerdef bad_multiple_yields(): yield 1 yield 2 # 这个永远不会执行with bad_multiple_yields() as value: print(value) # 输出:1
和传统类上下文管理器不同(__exit__ 返回 True 可屏蔽异常),@contextmanager 装饰的上下文,默认不会屏蔽异常,异常会直接传播出去。如果需要屏蔽异常,必须手动捕获。
from contextlib import contextmanager# 不会屏蔽异常(异常会传播出去)@contextmanagerdef does_not_suppress(): try: yield finally: pass# 手动捕获,实现异常屏蔽@contextmanagerdef suppress_errors(): try: yield except Exception as e: print(f"屏蔽异常:{e}") # 不重新抛出,就是屏蔽# 测试with suppress_errors(): raise ValueError("这个异常会被屏蔽")print("程序继续执行") # 会正常执行,不会被异常中断
总之,@contextmanager 装饰器的核心价值,就是“用生成器简化上下文管理器的实现”,让代码更简洁。还是那句话,说再多不如自己亲手尝试。