Python中上下文管理器:contextmanager、asynccontextmanager的作用详解,一文带你弄懂。
结论:
整体来说,contextmanager、asynccontextmanager这两个就是个语法糖,
只是 简化了 代码重复使用、还有防止忘记上下文(开关文件、网络连接等)常见的关闭、让其具有通用性。
1. contextlib 的概念
contextlib 是 Python 的一个内置模块,专门用来简化“上下文管理”的操作。
上下文管理指的是在代码执行时,需要对某些资源(比如文件、网络连接、锁等)进行管理,确保它们在用完后被正确地清理或释放。
Python 中最常见的上下文管理工具就是 with 语句,而 contextlib 提供了一些工具,让你更方便地定义和管理这种行为。
2. 作用
- 简化资源管理:帮助你自动处理资源的分配和释放,比如打开文件后自动关闭。
- 减少样板代码:让你少写重复的 try/finally 块。
- 支持同步和异步:像 asynccontextmanager 这样的工具,专门为异步编程提供支持。
- 提高代码可读性:让代码更简洁,逻辑更清晰。
3. 通俗解释
想象你去借书,借的时候得登记,归还的时候也得登记。如果每次借书还书都要自己手动写一堆登记代码,会很麻烦。contextlib 就像一个“图书管理员助手”,它帮你自动完成“借”和“还”的流程,你只需要告诉它“借什么书”和“用完怎么还”,剩下的事它全包了。
在 Python 中,这个“借和还”的过程通常是用 with 语句实现的,而 contextlib 提供了一些工具,让你轻松定义自己的“借还规则”。
1. 同步场景:文件操作日志
场景描述
假设我们要写一个程序,每次操作文件时需要记录日志(比如进入和退出时的状态),并确保文件正确关闭。
没使用 contextmanager 的版本
import logging logging.basicConfig(level=logging.INFO) def process_file(filename): # 手动管理资源 file = None try: logging.info("准备打开文件") file = open(filename, 'r') content = file.read() logging.info("文件读取完成") return content except Exception as e: logging.error(f"发生错误: {e}") raise finally: if file: file.close() logging.info("文件已关闭") # 使用 print(process_file("example.txt"))
问题与不便:
- 代码冗长:每次操作文件都要写 try/finally 来确保文件关闭。
- 容易出错:如果忘记写 finally 或关闭逻辑,文件资源会泄漏。
- 重复性高:如果多个地方需要类似操作,得重复写类似的样板代码。
使用 contextmanager 的版本
from contextlib import contextmanager import logging logging.basicConfig(level=logging.INFO) @contextmanager def file_handler(filename): logging.info("准备打开文件") file = open(filename, 'r') try: yield file finally: file.close() logging.info("文件已关闭") # 使用 def process_file(filename): with file_handler(filename) as f: content = f.read() logging.info("文件读取完成") return content print(process_file("example.txt"))
INFO:root:准备打开文件
INFO:root:文件读取完成
INFO:root:文件已关闭
Hello
带来的便利:
- 简洁:去掉了显式的 try/finally,代码更紧凑。
- 可复用:file_handler 可以被多个函数调用,避免重复写关闭逻辑。
- 安全:资源管理交给 contextmanager,保证文件一定会被关闭。
- 可读性强:逻辑集中在 with 块中,意图更清晰。
2. 异步场景:异步 HTTP 请求
场景描述
假设我们要用 aiohttp 做一个异步 HTTP 请求,请求一个 API,并在请求前后记录状态(比如连接和断开),确保会话正确关闭。
没使用 asynccontextmanager 的版本
import aiohttp import asyncio import logging logging.basicConfig(level=logging.INFO) async def fetch_data(url): session = None try: logging.info("准备发起请求") session = aiohttp.ClientSession() async with session.get(url) as response: data = await response.text() logging.info("请求完成") return data except Exception as e: logging.error(f"请求失败: {e}") raise finally: if session: await session.close() logging.info("会话已关闭") # 运行 async def main(): data = await fetch_data("https://api.example.com") print(data) asyncio.run(main())
问题与不便:
- 手动管理麻烦:需要显式创建 session,并在 finally 中关闭。
- 容易遗漏:如果忘记关闭 session,会导致资源泄漏(比如网络连接未释放)。
- 代码重复:如果多个地方需要发起请求,每次都要写类似的清理逻辑。
使用 asynccontextmanager 的版本
from contextlib import asynccontextmanager import aiohttp import asyncio import logging logging.basicConfig(level=logging.INFO) @asynccontextmanager async def http_session(): logging.info("准备发起请求") session = aiohttp.ClientSession() try: yield session finally: await session.close() logging.info("会话已关闭") # 使用 async def fetch_data(url): async with http_session() as session: async with session.get(url) as response: data = await response.text() logging.info("请求完成") return data async def main(): data = await fetch_data("https://api.example.com") print(data) asyncio.run(main())
INFO:root:准备发起请求 INFO:root:请求完成 INFO:root:会话已关闭 Hello from API
带来的便利:
- 简洁优雅:去掉了手动 try/finally,代码更简洁。
- 资源安全:asynccontextmanager 确保 session 一定会被关闭,无需手动管理。
- 复用性强:http_session 可以被多个异步函数复用,减少重复代码。
- 异步支持:完美适配异步编程,配合 async with 使用自然。
总结:
contextmanager 适合同步场景,比如文件、数据库连接。
asynccontextmanager 适合异步场景,比如网络请求、异步 I/O。
追问:为什么用这两个装饰器修饰的内部,都需要用到 yield?
1. 为什么需要 yield?
简单来说,yield 是 Python 中实现“上下文管理协议”的关键。它把一个函数分成两部分:
- yield 之前:进入上下文时执行的代码(资源分配)。
- yield 之后:离开上下文时执行的代码(资源释放)。
- yield 的值:交给 with 语句使用的资源。
contextmanager 和 asynccontextmanager 的作用就是利用 yield 的这种“暂停和恢复”特性,把一个普通的生成器函数(或异步生成器函数)变成一个上下文管理器,自动适配 with 或 async with 语句。
2. 通俗解释
想象你去餐厅吃饭:
- 进入餐厅(yield 之前):服务员给你安排座位、递上菜单。
- 用餐时间(yield 的值):你拿到食物,开始吃(这是 with 块里用的东西)。
- 离开餐厅(yield 之后):吃完后服务员收拾桌子、结账。
yield 就像是服务员把食物递给你时的一个“暂停点”。它把“准备”和“清理”分开了,中间的部分(吃东西)交给顾客(with 块)处理。contextmanager 就像餐厅的自动化管理系统,确保服务员按顺序完成这些步骤。
3. 技术上的原因
在 Python 中,with 语句依赖于“上下文管理协议”,通常通过类实现,需要定义 __enter__ 和 __exit__ 方法。但写类太麻烦,contextmanager 提供了一种更简单的方式:用生成器函数替代。
- 当 with 语句执行时:
- 调用被 @contextmanager 修饰的函数,运行到 yield 并暂停。
- yield 返回的值赋给 with ... as 的变量。
- 执行 with 块内的代码。
- with 块结束后,自动回到 yield 之后的代码继续执行。
asynccontextmanager 同理,只是它支持异步操作,用在 async with 中。
如果没有 yield,函数就无法“暂停”并返回值给 with,也就无法区分“进入”和“退出”的逻辑。

浙公网安备 33010602011771号