Python中上下文管理器:contextmanager、asynccontextmanager的作用详解,一文带你弄懂。

结论:

整体来说,contextmanager、asynccontextmanager这两个就是个语法糖,

只是 简化了 代码重复使用、还有防止忘记上下文(开关文件、网络连接等)常见的关闭、让其具有通用性。

1. contextlib 的概念

contextlib 是 Python 的一个内置模块,专门用来简化“上下文管理”的操作。

上下文管理指的是在代码执行时,需要对某些资源(比如文件、网络连接、锁等)进行管理,确保它们在用完后被正确地清理或释放。

Python 中最常见的上下文管理工具就是 with 语句,而 contextlib 提供了一些工具,让你更方便地定义和管理这种行为。


2. 作用

  • 简化资源管理:帮助你自动处理资源的分配和释放,比如打开文件后自动关闭。
  • 减少样板代码:让你少写重复的 try/finally 块。
  • 支持同步和异步:像 asynccontextmanager 这样的工具,专门为异步编程提供支持。
  • 提高代码可读性:让代码更简洁,逻辑更清晰。

3. 通俗解释

想象你去借书,借的时候得登记,归还的时候也得登记。如果每次借书还书都要自己手动写一堆登记代码,会很麻烦。contextlib 就像一个“图书管理员助手”,它帮你自动完成“借”和“还”的流程,你只需要告诉它“借什么书”和“用完怎么还”,剩下的事它全包了。

在 Python 中,这个“借和还”的过程通常是用 with 语句实现的,而 contextlib 提供了一些工具,让你轻松定义自己的“借还规则”。

 

下面我将通过两个真实的例子,
分别展示 contextmanager 和 asynccontextmanager 在没使用和使用后的差异,
并说明它们带来的便利,每个例子都会尽量贴近实际应用场景。

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"))

问题与不便:

  1. 代码冗长:每次操作文件都要写 try/finally 来确保文件关闭。
  2. 容易出错:如果忘记写 finally 或关闭逻辑,文件资源会泄漏。
  3. 重复性高:如果多个地方需要类似操作,得重复写类似的样板代码。

使用 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"))
输出(假设 example.txt 内容为 "Hello"):
INFO:root:准备打开文件
INFO:root:文件读取完成
INFO:root:文件已关闭
Hello

带来的便利:

  1. 简洁:去掉了显式的 try/finally,代码更紧凑。
  2. 可复用file_handler 可以被多个函数调用,避免重复写关闭逻辑。
  3. 安全:资源管理交给 contextmanager,保证文件一定会被关闭。
  4. 可读性强:逻辑集中在 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())

问题与不便:

  1. 手动管理麻烦:需要显式创建 session,并在 finally 中关闭。
  2. 容易遗漏:如果忘记关闭 session,会导致资源泄漏(比如网络连接未释放)。
  3. 代码重复:如果多个地方需要发起请求,每次都要写类似的清理逻辑。

使用 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())
输出(假设 API 返回 "Hello from API"):
INFO:root:准备发起请求
INFO:root:请求完成
INFO:root:会话已关闭
Hello from API

带来的便利:

  1. 简洁优雅:去掉了手动 try/finally,代码更简洁。
  2. 资源安全asynccontextmanager 确保 session 一定会被关闭,无需手动管理。
  3. 复用性强http_session 可以被多个异步函数复用,减少重复代码。
  4. 异步支持:完美适配异步编程,配合 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 语句执行时:
    1. 调用被 @contextmanager 修饰的函数,运行到 yield 并暂停。
    2. yield 返回的值赋给 with ... as 的变量。
    3. 执行 with 块内的代码。
    4. with 块结束后,自动回到 yield 之后的代码继续执行。

asynccontextmanager 同理,只是它支持异步操作,用在 async with 中。

如果没有 yield,函数就无法“暂停”并返回值给 with,也就无法区分“进入”和“退出”的逻辑。

 

posted @ 2025-03-10 14:37  AlphaGeek  阅读(663)  评论(0)    收藏  举报