Python的contextlib与 FastAPI 中的上下文管理

contextlib 是 Python 标准库里一组围绕上下文管理器设计的工具。它解决的核心问题很朴素:某项资源在使用前需要准备,使用结束后必须清理,而且无论中途是否抛出异常,清理动作都不能漏掉。

数据库连接、文件句柄、锁、网络会话、临时目录,以及 FastAPI 应用启动时加载的模型,都属于这种有明确生命周期的资源。contextlib 的价值,就是把这些成对出现的初始化与清理动作放在同一个结构中,减少资源泄漏和异常路径遗漏。


一、contextlib 究竟是什么

Python 的 with 语句依赖一种名为上下文管理器的协议。最常见的例子是文件操作。

python 复制代码
with open("data.txt", "r", encoding="utf-8") as file:
    content = file.read()

进入 with 代码块时,文件被打开;离开代码块时,文件会被关闭。即使读取过程中发生异常,关闭动作仍会执行。

上下文管理协议

同步上下文管理器包含两个特殊方法。

python 复制代码
class ManagedResource:
    def __enter__(self):
        print("获取资源")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("释放资源")

使用方式如下。

python 复制代码
with ManagedResource() as resource:
    print("使用资源")

它的执行过程可以理解为:

flowchart TD A[进入 with] --> B[调用 __enter__] B --> C[执行代码块] C --> D{是否发生异常} D -->|没有| E[调用 __exit__] D -->|发生| F[将异常信息传给 __exit__] F --> G{异常是否被抑制} G -->|是| H[继续运行] G -->|否| I[向外抛出异常] E --> H

__exit__ 会收到异常类型、异常对象和追踪信息。它返回真值时,异常会被视为已经处理;返回假值或 None 时,异常继续向外传播。

异步场景采用对应的协议:

  • __aenter__
  • __aexit__
  • async with

PEP 343 定义了 with 语句及其上下文管理机制,而 contextlib 在这套协议之上提供了更便捷、更易组合的工具。


二、contextlib 涉及的核心概念

理解 contextlib,关键是抓住三个词:生命周期、异常安全和组合管理

1.资源生命周期

很多资源都有三个阶段:

  • 创建或获取资源;
  • 使用资源;
  • 释放、关闭或回滚资源。

如果手写 try...finally,通常会是这样。

python 复制代码
resource = acquire_resource()

try:
    use_resource(resource)
finally:
    release_resource(resource)

上下文管理器把同样的逻辑收进 with 中。

python 复制代码
with managed_resource() as resource:
    use_resource(resource)

两者在控制流程上很接近,但上下文管理器更适合复用,也更容易嵌套和组合。

2.异常安全

清理代码不能只放在正常路径上。例如下面的写法存在风险。

python 复制代码
connection = open_connection()
run_query(connection)
connection.close()

一旦 run_query() 抛出异常,close() 就不会运行。

上下文管理器在语义上接近 try...finally,可以保证离开作用域时执行清理逻辑。这里的离开既包括正常结束,也包括异常、returnbreak 等控制流程。

3.生成器式上下文管理器

手写 __enter____exit__ 有时略显笨重。contextlib.contextmanager 可以把一个生成器函数转换成同步上下文管理器。

python 复制代码
from contextlib import contextmanager


@contextmanager
def managed_connection():
    connection = open_connection()

    try:
        yield connection
    finally:
        connection.close()

调用时仍然使用普通的 with

python 复制代码
with managed_connection() as connection:
    run_query(connection)

这里的 yield 是整个结构的分界线:

  • yield 之前负责初始化;
  • yield 返回的值交给 as 后面的变量;
  • yield 之后负责清理;
  • yield 在一次上下文执行中只能出现一次。

如果 with 内部抛出异常,该异常会在 yield 所在位置重新出现。因此,生成器内部既可以通过 finally 清理资源,也可以捕获并处理特定异常。

4.异步上下文管理器

数据库连接池、异步 HTTP 客户端等资源,初始化和关闭过程本身可能需要 await。此时可以使用 asynccontextmanager

python 复制代码
from contextlib import asynccontextmanager


@asynccontextmanager
async def managed_client():
    client = await create_client()

    try:
        yield client
    finally:
        await client.close()

调用方式是:

python 复制代码
async with managed_client() as client:
    await client.send_request()

FastAPI 是异步 Web 框架,因此 asynccontextmanager 在应用生命周期管理中十分常见。


三、contextlib 中常用的工具

contextlib 不只有两个装饰器。它还提供了一组适合不同资源管理场景的实用工具。

工具 主要用途 典型场景
contextmanager 用生成器创建同步上下文管理器 文件、事务、锁
asynccontextmanager 用异步生成器创建异步上下文管理器 数据库连接池、HTTP 客户端
closing 离开作用域时调用对象的 close() 不支持上下文协议的第三方对象
aclosing 离开异步作用域时调用 aclose() 异步生成器、异步流
suppress 有选择地忽略异常 删除可能不存在的文件
nullcontext 提供一个什么也不做的上下文 可选的上下文管理
redirect_stdout 临时重定向标准输出 捕获命令输出、测试
ExitStack 动态管理多个同步上下文 资源数量运行时才能确定
AsyncExitStack 混合管理异步上下文和清理回调 多个异步客户端或连接

suppress 有选择地忽略异常

python 复制代码
from contextlib import suppress
from pathlib import Path


with suppress(FileNotFoundError):
    Path("cache.tmp").unlink()

这相当于只捕获并忽略 FileNotFoundError。不应写成过于宽泛的异常抑制,否则真正的程序错误也可能悄悄溜走。

nullcontext 处理可选资源

假设调用者有时传入已打开的文件,有时传入文件路径,可以用 nullcontext 统一控制流程。

python 复制代码
from contextlib import nullcontext


def read_data(file_object=None):
    context = (
        nullcontext(file_object)
        if file_object is not None
        else open("data.txt", "r", encoding="utf-8")
    )

    with context as file:
        return file.read()

ExitStack 动态组合资源

普通 with 更适合资源数量固定的情况。

python 复制代码
with open("a.txt") as file_a, open("b.txt") as file_b:
    ...

如果文件数量在运行时才知道,ExitStack 更自然。

python 复制代码
from contextlib import ExitStack


paths = ["a.txt", "b.txt", "c.txt"]

with ExitStack() as stack:
    files = [
        stack.enter_context(open(path, "r", encoding="utf-8"))
        for path in paths
    ]

    contents = [file.read() for file in files]

退出 ExitStack 时,已经成功进入的上下文会按照后进先出的顺序关闭。即使打开第三个文件时发生异常,前两个文件仍能得到妥善关闭。

AsyncExitStack 是它的异步版本,适合 FastAPI 中需要动态组合多个异步资源的场景。


四、FastAPI 中最重要的用途------应用生命周期

FastAPI 服务往往需要在开始接收请求前完成初始化,在停止服务时释放资源。例如:

  • 建立数据库连接池;
  • 创建 Redis 客户端;
  • 创建共享的异步 HTTP 客户端;
  • 加载机器学习模型;
  • 启动后台任务;
  • 在关闭时刷新缓冲区并断开连接。

FastAPI 推荐通过 lifespan 参数接收一个异步上下文管理器yield 之前是启动阶段,yield 之后是关闭阶段。

基本示例

python 复制代码
from contextlib import asynccontextmanager

from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI):
    print("应用启动,初始化资源")

    app.state.model = load_model()

    try:
        yield
    finally:
        print("应用关闭,释放资源")
        app.state.model = None


app = FastAPI(lifespan=lifespan)

整体流程如下。

flowchart TD A[进程启动] --> B[进入 lifespan] B --> C[执行 yield 之前的初始化] C --> D[FastAPI 开始接收请求] D --> E[持续处理请求] E --> F[服务器准备关闭] F --> G[执行 yield 之后的清理] G --> H[退出进程]

yield 之前的代码执行完成后,FastAPI 才开始处理请求。服务器收到关闭信号后,会继续执行 yield 后面的代码。

管理数据库连接池

下面用抽象接口展示结构,具体方法会随数据库驱动变化。

python 复制代码
from contextlib import asynccontextmanager

from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI):
    pool = await create_database_pool()
    app.state.db_pool = pool

    try:
        yield
    finally:
        await pool.close()


app = FastAPI(lifespan=lifespan)

业务代码可以从 request.app.state 读取连接池。

python 复制代码
from fastapi import Request


@app.get("/users")
async def list_users(request: Request):
    pool = request.app.state.db_pool

    async with pool.acquire() as connection:
        return await connection.fetch("SELECT * FROM users")

这里存在两层生命周期:

  • 数据库连接池与整个 FastAPI 应用同生共死;
  • 单个数据库连接只服务于当前查询或请求。

将不同生命周期分开,代码会清楚许多,也能避免每个请求都重复创建昂贵的连接池。


五、管理多个 FastAPI 共享资源

真实项目通常不止一个共享资源。可以直接嵌套多个 async with,也可以使用 AsyncExitStack 集中管理。

python 复制代码
from contextlib import AsyncExitStack, asynccontextmanager

from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI):
    async with AsyncExitStack() as stack:
        database = await stack.enter_async_context(
            create_database_context()
        )
        http_client = await stack.enter_async_context(
            create_http_client_context()
        )
        redis = await stack.enter_async_context(
            create_redis_context()
        )

        app.state.database = database
        app.state.http_client = http_client
        app.state.redis = redis

        yield

AsyncExitStack 会记住成功注册的资源,并在退出时按相反顺序清理。假如 Redis 初始化失败,已经建立的 HTTP 客户端和数据库资源仍会被关闭。

这类结构尤其适合以下情况:

  • 资源是否启用由配置决定;
  • 资源数量在运行时变化;
  • 同时存在同步和异步清理逻辑;
  • 初始化过程可能在中途失败;
  • 希望把清理责任集中到一个出口。

示意流程如下。

flowchart TD A[创建 AsyncExitStack] --> B[注册数据库] B --> C[注册 HTTP 客户端] C --> D[注册 Redis] D --> E[应用运行] E --> F[关闭 Redis] F --> G[关闭 HTTP 客户端] G --> H[关闭数据库]

资源关闭顺序通常与创建顺序相反,类似叠盘子:后放上去的盘子先拿下来。


六、在 FastAPI 依赖注入中的用法

FastAPI 的 yield 依赖与上下文管理器有相似的生命周期语义。依赖在 yield 前获取资源,将资源交给路径函数,并在请求结束后执行 yield 后的清理代码。

请求级数据库会话

python 复制代码
from collections.abc import AsyncIterator

from fastapi import Depends, FastAPI


app = FastAPI()


async def get_database_session() -> AsyncIterator[DatabaseSession]:
    session = DatabaseSession()

    try:
        yield session
    finally:
        await session.close()


@app.get("/users/{user_id}")
async def get_user(
    user_id: int,
    session: DatabaseSession = Depends(get_database_session),
):
    return await session.get_user(user_id)

这类依赖通常不必再添加 @asynccontextmanager。FastAPI 能直接识别带 yield 的依赖函数,并负责驱动它的进入与退出过程。

复用已有的异步上下文管理器

如果资源管理逻辑还需要在 FastAPI 之外使用,可以先定义独立上下文管理器,再通过依赖包装。

python 复制代码
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager


@asynccontextmanager
async def database_session_context():
    session = DatabaseSession()

    try:
        yield session
    finally:
        await session.close()


async def get_database_session() -> AsyncIterator[DatabaseSession]:
    async with database_session_context() as session:
        yield session

这种分层方式很实用:

  • database_session_context 是通用资源管理工具;
  • get_database_session 负责接入 FastAPI 依赖系统;
  • 命令行任务、后台任务和测试也能复用同一套资源管理逻辑。

应用级与请求级资源的区别

资源级别 存活时间 适合管理的对象 常见实现
应用级 从服务启动到关闭 连接池、模型、共享客户端 lifespan
请求级 从请求开始到请求结束 数据库会话、事务、临时文件 yield 依赖
局部代码块 某段操作执行期间 单个连接、锁、文件 withasync with

判断资源应该放在哪里,主要看它的生命周期。创建成本高且能够安全共享的对象适合放入 lifespan;与单次请求状态紧密相关的对象,更适合放入依赖函数。


七、事务管理与异常回滚

上下文管理器非常适合表达数据库事务的语义:成功则提交,失败则回滚。

python 复制代码
from contextlib import asynccontextmanager


@asynccontextmanager
async def transaction(session):
    try:
        yield session
    except Exception:
        await session.rollback()
        raise
    else:
        await session.commit()

在 FastAPI 依赖中复用:

python 复制代码
async def get_transactional_session():
    async with create_session() as session:
        async with transaction(session):
            yield session

业务函数只需关注业务逻辑。

python 复制代码
@app.post("/orders")
async def create_order(
    payload: OrderCreate,
    session=Depends(get_transactional_session),
):
    order = await insert_order(session, payload)
    await update_inventory(session, payload.items)
    return order

如果库存更新失败,异常会沿调用栈传播到事务上下文,触发回滚。回滚完成后再次 raise,让 FastAPI 的异常处理机制继续处理错误。

除非确实要把异常视为成功,否则不要在上下文管理器中捕获异常后保持沉默。资源是清理干净了,但错误也可能因此被藏进地毯下面。


八、测试中的用途

FastAPI 的 TestClient 本身也可以作为上下文管理器使用。需要测试应用启动与关闭逻辑时,应通过 with 进入客户端。

python 复制代码
from fastapi.testclient import TestClient


def test_application_lifespan():
    with TestClient(app) as client:
        response = client.get("/health")
        assert response.status_code == 200

进入 with TestClient(app) 时,测试客户端执行 FastAPI 的启动生命周期;退出时执行关闭生命周期。

如果只实例化客户端却不进入上下文,测试环境中的 lifespan 可能不会按照预期运行。

python 复制代码
client = TestClient(app)

因此,测试数据库连接池、模型加载、缓存初始化等启动行为时,推荐使用上下文形式。


九、常见误区与实践建议

把短生命周期资源放进全局状态

数据库会话、事务和临时文件通常不应放在 app.state 中长期共享。它们往往携带请求状态,并发共享可能产生数据污染或事务混乱。

更合理的结构是:

  • app.state 保存线程安全或协程安全的长期资源;
  • FastAPI 依赖保存请求级资源;
  • 局部 with 保存操作级资源。

忘记在 yield 后清理

下面的上下文管理器没有释放资源。

python 复制代码
@asynccontextmanager
async def broken_context():
    client = await create_client()
    yield client

更稳妥的写法是使用 try...finally

python 复制代码
@asynccontextmanager
async def managed_context():
    client = await create_client()

    try:
        yield client
    finally:
        await client.close()

在生命周期中执行永久阻塞任务

yield 之前的代码必须执行完毕,应用才能开始接收请求。如果直接等待一个永不结束的任务,FastAPI 就会一直停留在启动阶段。

后台任务应创建为独立任务,并在关闭阶段取消和等待。

python 复制代码
import asyncio
from contextlib import asynccontextmanager, suppress


@asynccontextmanager
async def lifespan(app):
    task = asyncio.create_task(background_worker())

    try:
        yield
    finally:
        task.cancel()

        with suppress(asyncio.CancelledError):
            await task

混淆同步和异步上下文

同步上下文使用:

python 复制代码
with resource:
    ...

异步上下文使用:

python 复制代码
async with resource:
    ...

若资源的进入或退出需要 await,就应实现异步上下文协议,并使用 asynccontextmanager。不能仅因为路径函数写成了 async def,就把所有同步对象都改成 async with。对象支持哪一种协议,才是判断依据。


结语

contextlib 可以看作 Python 的资源生命周期工具箱。它把初始化、使用、异常处理和清理收拢在清晰的作用域中,让代码在正常路径和异常路径上保持一致。

在 FastAPI 项目里,可以按生命周期划分职责:

  • 应用启动到关闭的共享资源交给 lifespanasynccontextmanager
  • 单次请求使用的会话、事务交给带 yield 的依赖;
  • 数量动态变化的资源交给 ExitStackAsyncExitStack
  • 测试生命周期时,用 with TestClient(app) 驱动启动和关闭事件。

掌握这套思路后,contextlib 就不再只是几个装饰器。它会成为组织数据库、缓存、网络客户端、后台任务和模型资源的一条主线,让 FastAPI 服务更可靠,也让清理代码不再四处救火。

参考资料

Python Documentation contextlib Utilities for with Statement Contexts

docs.python.org/3/library/c...

PEP 343 The with Statement

peps.python.org/pep-0343/

FastAPI Documentation Lifespan Events

fastapi.tiangolo.com/advanced/ev...

FastAPI Documentation Testing Events Lifespan and Startup Shutdown

fastapi.tiangolo.com/advanced/te...

相关推荐
用户9385156350743 分钟前
从同源策略到Nginx代理:React+NestJS全栈项目跨域实战
后端·docker
测试19981 小时前
接口自动化测试的全面解析与实战指南
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·接口测试
whcyhhh1 小时前
头歌实践教学平台:数据科学与大数据技术导论(九上)
大数据·python
卷无止境1 小时前
FastAPI Events 深度解析与工程实践
后端·python·fastapi
FYKJ_20101 小时前
springboot网上购书商城---附源码15749
java·spring boot·后端·python·spark·django·php
青 春 记 忆1 小时前
LeetCode 234. 回文链表|Python 解法详解
python·leetcode·链表
招财小梗1 小时前
沈阳AI企业服务真能降本增效?
大数据·人工智能·python
敲代码还房贷1 小时前
安装wsl报错:HCS_E_HYPERV_NOT_INSTALLED
linux·python·ubuntu
whcyhhh2 小时前
头歌实践教学平台:数据科学与大数据技术导论(九下)
大数据·python