首页 / FastAPI 入门教程 / 带 yield 的依赖与资源清理

FastAPI 入门教程

带 yield 的依赖与资源清理

本教程共 50 篇 · 第 25 篇 · 更新于 2026-08-12 · 约 7 分钟阅读

FastAPIFastAPI 入门教程yield依赖资源清理tryfinally异步依赖异常安全上下文管理

本节目标:学会用 yield 写依赖,在请求开始时创建资源、请求结束后自动清理,并用 try/finally 保证资源释放不被异常打断。

前面讲的依赖都用 return 把值交出去。可有些资源不是”交出去就完事”:数据库连接用完得关,文件打开后得关,锁用完得释放。

这些资源都要”善后”。FastAPI 的 yield 依赖就是干这个的:yield 之前准备资源,yield 把资源交出去,yield 之后写清理代码。

25-1 yield 依赖长什么样

还是以数据库会话为例。注意这里用 yield 而不是 return

from collections.abc import Iterator

from typing import Annotated

from fastapi import Depends, HTTPException

app = FastAPI()


def get_db() -> Iterator["DBSession"]:
    db = DBSession()
    try:
        yield db
    finally:
        db.close()


@app.get("/items/")
async def read_items(
    db: Annotated["DBSession", Depends(get_db)],
) -> list:
    return db.execute("SELECT * FROM items")

执行顺序是这样的:

  1. 请求进来,先跑 yield 之前的代码,创建 db 连接
  2. 遇到 yield db,把连接交给路径操作函数(以及别的需要它的依赖)
  3. 路径操作函数用完后,再跑 yield 之后的代码,这里 db.close() 关闭连接

也就是说,yield 把”前”和”后”切成两段:前段准备,后段收尾。

Tip

一个依赖里只能有一个 yield。别在一个依赖函数里写多个 yield,那会出错。

25-2 为什么用 try/finally

资源清理最怕中途抛异常。假如路径操作里查询数据库报错,清理代码还会跑吗?会,因为我们用了 try/finally

def get_db() -> Iterator["DBSession"]:
    db = DBSession()
    try:
        yield db
    finally:
        db.close()

无论 yield db 之后、路径操作执行期间发生什么,finally 块里的 db.close() 都会执行。这样连接一定会被关掉,不会被泄露。

如果路径操作正常结束,finally 照常跑;如果路径操作抛了异常,finally 还是跑。这就是”异常安全”的资源清理。

25-3 异步依赖也能用 yield

如果你的依赖里有 await(比如异步连接数据库),就用 async def 配合 yield,完全没问题:

from collections.abc import AsyncIterator

from typing import Annotated

from fastapi import Depends


async def get_db() -> AsyncIterator["AsyncDBSession"]:
    db = await AsyncDBSession.connect()
    try:
        yield db
    finally:
        await db.close()


@app.get("/items/")
async def read_items(
    db: Annotated["AsyncDBSession", Depends(get_db)],
) -> list:
    return await db.execute("SELECT * FROM items")

FastAPI 会区分同步和异步依赖,分别用对的方式处理。你写 async def 还是 def,框架都能接住。教程默认推荐 async def,遇到阻塞 I/O 才考虑 def

Note

技术上讲,FastAPI 内部把 yield 依赖包装成了 Python 的上下文管理器(context manager),所以行为和 with 语句一致:进去创建、出来清理,中间出错也清理。

25-4 yield 依赖里捕获异常

如果路径操作或别的依赖在处理时抛了异常,这个异常会”冒”进你的 yield 依赖。你可以在 yield 外面用 try/except 接住它。

比如数据库事务中途被回滚,你可以捕获到异常再做处理:

def get_db() -> Iterator["DBSession"]:
    db = DBSession()
    try:
        yield db
    except Exception:
        db.rollback()
        raise
    finally:
        db.close()

这里 except 捕获到异常后先 rollback() 回滚,再用 raise 把异常原样抛出去。注意:抛出去很重要,否则 FastAPI 不知道出错了。

25-5 捕获后一定要再抛出

这是个容易踩的坑。如果你在 yield 依赖里用 except 接住异常,却既不重新抛出、也不抛新异常,FastAPI 就以为一切正常。客户端会收到一个 500 错误,但你的服务器日志里什么痕迹都没有,排查极难。

正确做法是重新抛出原异常:

def get_db() -> Iterator["DBSession"]:
    db = DBSession()
    try:
        yield db
    except Exception as exc:
        raise HTTPException(status_code=500, detail="数据库出错了") from exc
    finally:
        db.close()

这样客户端照样收到错误,服务器日志也记录了你的自定义错误,方便定位。除非你确实要抛一个全新的 HTTPException,否则务必把原异常再 raise 出去。

25-6 子依赖也能用 yield

依赖树里任何一层都能用 yield,而且能混着用:yield 的依赖和 return 的依赖可以共存,子依赖也能套子依赖。

from collections.abc import Iterator

from typing import Annotated

from fastapi import Depends, HTTPException


def dependency_a() -> Iterator[str]:
    print("A: 开始")
    try:
        yield "a"
    finally:
        print("A: 结束")


def dependency_b(
    dep_a: Annotated[str, Depends(dependency_a)],
) -> Iterator[str]:
    print("B: 开始")
    try:
        yield f"b+{dep_a}"
    finally:
        print("B: 结束")


@app.get("/demo/")
async def demo(
    dep_b: Annotated[str, Depends(dependency_b)],
) -> dict:
    return {"value": dep_b}

FastAPI 会保证退出代码的顺序正确:最外层 dependency_b 的后段清理,需要 dependency_a 的资源还在,所以 A: 结束 会晚于 B: 结束 执行。整个过程被框架自动排好序,你不用管。

25-7 执行时机的细节

理解 yield 依赖的执行时机很重要:yield 之后的清理代码,默认在响应发送给客户端之后才跑。也就是说,资源会一直持有到响应发出。

大多数场景这没问题。但 FastAPI 也提供 scope 参数微调时机:

  • Depends(scope="request"):这是默认。依赖在请求前开始,yield 后的代码在响应发完后执行,环绕”整个请求-响应周期”。
  • Depends(scope="function"):依赖在路径操作函数前开始,yield 后的代码在路径操作函数返回后、响应发出前执行,只环绕”路径操作函数”。
from collections.abc import Iterator

from typing import Annotated

from fastapi import Depends, HTTPException


def get_conn() -> Iterator["Conn"]:
    conn = Conn()
    try:
        yield conn
    finally:
        conn.close()


@app.get("/x/")
async def x(
    conn: Annotated["Conn", Depends(get_conn, scope="function")],
) -> dict:
    return conn.data()

scope="function" 适合那种”只在路径操作函数里用一下、用完就想立刻释放”的资源,能少占用连接一会儿。默认用 "request" 就好,除非你有明确理由。

25-8 yield 依赖适合什么

总结一下,yield 依赖最适合管理”需要配对开关”的资源:

  • 数据库连接 / 会话:打开与关闭
  • 文件句柄:打开与关闭
  • 网络客户端:创建与断开
  • 锁、信号量:获取与释放
  • 事务:开启与提交 / 回滚

这些资源只要”开”了,就一定要”关”。用 yieldtry/finally,能保证无论成功失败都关得掉。

25-9 小结

yield 的依赖把生命周期分成两段:yield 之前创建资源、交给路径操作用,yield 之后做清理。配 try/finally 保证异常时也清理,配 except 后可捕获异常但要记得 raise 出去。

同步依赖用 def + yield,异步依赖用 async def + yield,子依赖也能用 yield,框架会自动排好退出顺序。需要资源在响应前就释放时,用 Depends(scope="function")。依赖注入到这里就讲全了,后面你会看到它在鉴权、数据库里的实战用法。