FastAPI 进阶依赖指南:可参数化依赖(Callable 实例)与 `yield` 依赖生命周期演进详解
本篇技术指南围绕 FastAPI 官方文档中的“进阶依赖”(Advanced Dependencies)主题展开,聚焦两大核心能力:一是通过 callable(可调用)实例实现带参数的依赖,解决“同一套校验逻辑、不同固定参数”需要重复声明大量函数/类的问题;二是深入剖析 带 yield 的依赖在 scope、StreamingResponse、except、后台任务等场景下的生命周期演进与迁移建议。读完你将掌握可参数化依赖的完整写法,理解 Depends(scope="function") 与默认 scope="request" 的差异,并能在升级旧版 FastAPI 应用时准确处理 yield 依赖的资源释放语义。
文档源代码示例位于仓库 docs_src/dependencies 目录,实现源码可对照 fastapi/params.py 与 fastapi/dependencies 深入阅读。
一、从“固定依赖”到“可参数化依赖”
在 FastAPI 的依赖注入体系中,此前教程中看到的依赖都是一个 固定的函数或类——每次使用 Depends() 时,依赖本身的行为是确定的、写死的。
但在真实业务里,我们常常会遇到这样的需求:希望同一套校验/处理逻辑能接收不同的配置参数,从而避免为每一种参数组合声明大量几乎重复的函数或类。
原文档给出的是一个非常典型的场景:
我们希望有一个依赖,用于检查查询参数
q中是否包含某段“固定内容”。同时,我们又希望这段“固定内容”是可以参数化的。
如果按“固定函数/固定类”的思路,每换一个固定内容(例如 "bar"、"foo")就要新写一个函数或类,代码会迅速膨胀。而 Python 本身提供了一种优雅的机制来解决这个问题——类的可调用实例(callable instance)。
二、可调用实例(Callable Instance)实现参数化依赖
2.1 什么是“可调用的实例”
Python 中,类本身天然是可调用的(调用它即实例化)。但这里要强调的是:让某个类的“实例”也变得可调用,而非类本身。
实现方式是在类中声明一个 __call__ 方法。有了它,实例就能像函数一样被调用,例如 checker(...)。原文档对应的示例代码位于 docs_src/dependencies/tutorial011_an_py310.py,其核心逻辑如下:
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
class FixedContentQueryChecker:
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
def __call__(self, q: str = ""):
if q:
return self.fixed_content in q
return False
checker = FixedContentQueryChecker("bar")
@app.get("/query-checker/")
async def read_query_check(fixed_content_included: Annotated[bool, Depends(checker)]):
return {"fixed_content_in_query": fixed_content_included}
从 FastAPI 的角度看,这个 __call__ 方法有两重作用:
- 用来检查额外的参数和子依赖——FastAPI 会像解析普通依赖函数一样解析
__call__的签名(这里的q: str = ""); - 最终被调用来产出一个值——该返回值会作为依赖解析结果,注入到 path operation function 的对应参数中(此处的
fixed_content_included)。
2.2 通过 __init__ 参数化实例
关键技巧在于:用 __init__ 声明“实例级参数”来参数化依赖。
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
此时,__init__ 是纯 Python 层面的普通构造逻辑——FastAPI 永远不会去触碰或关心 __init__ 中的内容,它只由我们自己的代码在创建实例时直接调用。这正是“参数化”得以成立的分工:
__init__负责把配置(如fixed_content)注入实例,供你按场景定制;__call__负责接收请求相关的参数(如q),交由 FastAPI 当作依赖函数解析执行。
2.3 创建实例并作为依赖使用
先手工创建带固定参数的实例:
checker = FixedContentQueryChecker("bar")
此时该依赖实例内部已携带 "bar",保存在属性 checker.fixed_content 中。接下来,使用实例本身而不是类:
async def read_query_check(fixed_content_included: Annotated[bool, Depends(checker)]):
return {"fixed_content_in_query": fixed_content_included}
注意这里写的是 Depends(checker) 而不是 Depends(FixedContentQueryChecker)——依赖对象是实例 checker,而不是类。
FastAPI 在解析该依赖时,等价于执行:
checker(q="somequery")
q 来自真实请求的查询参数,返回值(布尔值,表示固定内容是否被包含)会被注入到 path operation function 的参数 fixed_content_included 中。若请求形如 GET /query-checker/?q=somequerybar,返回值即 {"fixed_content_in_query": true}。
2.4 真实世界同类实现:安全模块正是这样写的
原文档特别提示:这套写法看起来“刻意且复杂”,示例是为了展示原理而故意简化;在安全(Security)章节中,有一批工具函数/类正是以完全相同的方式实现。理解了 callable 实例依赖,就等于理解了这些安全工具底层的运作方式。
在 fastapi/security 源码中可找到大量证据。例如 fastapi/security/oauth2.py 中的 OAuth2PasswordBearer.__call__:
async def __call__(self, request: Request) -> str | None:
authorization = request.headers.get("Authorization")
if not authorization:
if self.auto_error:
raise self.make_not_authenticated_error()
else:
return None
return authorization
在教程中,用法正是“先参数化创建实例,再作为依赖注入”:
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
...
OAuth2PasswordBearer(tokenUrl="token") 就是在用 __init__ 参数化(配置 tokenUrl、auto_error 等),随后 FastAPI 调用其 __call__ 完成请求期校验。同样的 __call__ 模式还出现在 fastapi/security/http.py(如 HTTPBearer、HTTPBasic)与 fastapi/security/api_key.py(API Key Header/Query/Cookie)中,相关中文文档可查阅 docs/es/docs/tutorial/security 下的教程。若你还想查看其他可调用安全依赖如何被用作实例,可阅读 docs/es/docs/advanced/security/oauth2-scopes.md。
三、yield 依赖:scope、HTTPException、except 与后台任务的演进
3.1 这部分内容什么时候才用得上?
原文档开篇即给出明确警告:大多数应用并不需要这些技术细节。它们主要服务于两类人:
- 从 0.121.0 之前的旧版 FastAPI 应用升级、正遭遇
yield依赖相关问题的开发者; - 需要深刻理解依赖生命周期、以便排查资源释放时机的进阶用户。
带 yield 的依赖在 FastAPI 各版本中经历了一系列演进,用于覆盖不同用例并修复缺陷。下面按文档顺序梳理这些“历史变更”——它们直接决定了今天 yield 依赖的退出代码(yield 之后的清理逻辑)在何时执行。
3.2 yield 依赖与 scope(0.121.0 起支持)
FastAPI 0.121.0 起,为带 yield 的依赖新增了 Depends(scope="function") 支持。Depends 的 scope 字段在源码中定义为字面量类型(见 fastapi/params.py):
class Depends:
dependency: Callable[..., Any] | None = None
use_cache: bool = True
scope: Literal["function", "request"] | None = None
两种取值决定了依赖的“退出代码”(yield 之后的部分)何时执行:
scope 取值 |
依赖启动时机 | 退出代码执行时机 |
|---|---|---|
"function"(需要显式指定) |
处理该请求的 path operation function 执行前 | path operation function 结束后、响应发回客户端之前立刻执行 |
"request"(默认值) |
同上,请求处理前 | 响应已经发送给客户端之后再执行 |
示例(详见 docs_src/dependencies/tutorial008e_an_py310.py):
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
def get_username():
try:
yield "Rick"
finally:
print("Cleanup up before response is sent")
@app.get("/users/me")
def get_user_me(username: Annotated[str, Depends(get_username, scope="function")]):
return username
关于 scope 还有一条子依赖约束需要留意:
- 声明为
scope="request"(默认)的依赖,其所有子依赖也必须都是"request"作用域; - 而
scope="function"的依赖,其子依赖既可以是"function"也可以是"request"。
原因在于:任何依赖都需要能先于其子依赖执行退出代码,因为退出时它可能仍要用到子依赖提供的资源。
关于“提前退出(Early exit)与 scope”更完整的时序图与说明,可继续阅读 docs/es/docs/tutorial/dependencies/dependencies-with-yield.md 教程中的 “Salida temprana y scope” 一节。
3.3 yield 依赖与 StreamingResponse 的技术细节(0.118.0 回退)
在 0.118.0 之前,带 yield 的依赖会在 path operation function 返回后、发送响应之前就执行退出代码。这样设计的初衷是:避免在等待响应“走完网络”期间不必要地持有资源。
但这个设计带来了副作用:如果你返回的是 StreamingResponse,则依赖的退出代码可能已经提前执行完毕。举例来说,如果数据库会话放在 yield 依赖中,那么流式传输数据时该 StreamingResponse 将无法再使用这个会话——因为会话已在 yield 后的退出代码里被关闭。
0.118.0 对该行为进行了回退:让 yield 之后的退出代码改为在响应发送完成之后执行。
原文档附注指出:这一回退后的行为与 0.106.0 之前的旧行为非常相似,但针对若干边界情况做了改进与 bug 修复。
3.4 需要“提前退出”的特定用例:手动关闭会话
某些特定条件下,旧行为(发送响应前执行退出代码)反而更省资源。原文档给出一类典型场景:
代码在
yield依赖中使用数据库会话仅用于校验用户,path operation function 中不再使用该会话;同时响应传输很慢(如一个缓慢输出数据、且不使用数据库的StreamingResponse)。
此时若按默认 scope="request",数据库会话会被一直持有到响应完全发送完毕;但既然响应阶段根本用不到数据库,持有会话就属于浪费。
先看问题形态的示例(docs_src/dependencies/tutorial013_an_py310.py):
import time
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from sqlmodel import Field, Session, SQLModel, create_engine
engine = create_engine("postgresql+psycopg://postgres:postgres@localhost/db")
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
app = FastAPI()
def get_session():
with Session(engine) as session:
yield session
def get_user(user_id: int, session: Annotated[Session, Depends(get_session)]):
user = session.get(User, user_id)
if not user:
raise HTTPException(status_code=403, detail="Not authorized")
def generate_stream(query: str):
for ch in query:
yield ch
time.sleep(0.1)
@app.get("/generate", dependencies=[Depends(get_user)])
def generate(query: str):
return StreamingResponse(content=generate_stream(query))
这里的资源释放链路是:get_user 中抛出的 HTTPException(status_code=403, detail="Not authorized") 用于校验;get_session 中 yield 之后自动关闭 Session 的退出代码(示例第 19~21 行的 with 块结束逻辑),会等到慢速响应全部发送完之后才执行;而 generate_stream()(第 30~38 行,逐字符输出并 time.sleep(0.1) 模拟慢速流)并不使用数据库会话。
如果使用 SQLModel(或 SQLAlchemy)且恰好命中这类场景,可在不再需要会话时手动显式关闭(对比 docs_src/dependencies/tutorial014_an_py310.py,其差别在于校验完成后立即调用 session.close()):
def get_user(user_id: int, session: Annotated[Session, Depends(get_session)]):
user = session.get(User, user_id)
if not user:
raise HTTPException(status_code=403, detail="Not authorized")
session.close()
这样,会话会提前释放数据库连接,供其他请求复用。
需要强调的是:如果你的用例确实需要更通用的“从 yield 依赖中提前退出”机制,原文档建议在 FastAPI 的 Discussion 中提出具体用例。若存在足够有说服力的早期关闭(early closing)需求,上游会考虑新增一种“选择加入早期关闭”的新方式——也就是说,目前并没有一个通用开关来开启这一旧行为,手动关闭资源是文档给出的落地路径。
3.5 yield 依赖与 except 的技术细节(0.110.0 变更)
在 0.110.0 之前,如果在带 yield 的依赖中通过 except 捕获了异常,却没有再次 raise,该异常仍会被自动抛出/转发到任何异常处理器(exception handlers)或内部服务器错误处理器。
0.110.0 修改了这一行为,目的有两个:
- 修复“被转发的异常若没有对应处理器(即内部服务器错误)会导致未受控的内存消耗”的问题;
- 使其与 普通 Python 代码的惯例保持一致——捕获后不重新抛出,就应当视为已处理。
对升级用户而言,这意味着如果你在 except 块中捕获了异常且希望交给上层异常处理器统一处理,必须显式地重新抛出(raise)。
3.6 后台任务与 yield 依赖的技术细节(0.106.0 变更)
在 0.106.0 之前:yield 之后无法抛出异常,依赖退出代码在响应发送之后才执行,因此 异常处理器(自定义异常处理器)此时早已执行完毕。这样设计的初衷,主要是允许在后台任务中复用依赖 yield 出来的对象——因为退出代码会等后台任务结束后才执行。
0.106.0 修改了该行为,意图同样是“不在等待响应传输期间持有资源”。
原文档附带的建议非常实用:
后台任务通常是一组相互独立的逻辑,应当单独处理、自带资源(例如它自己的数据库连接)。这样做通常会得到更干净的代码。
因此,如果旧代码依赖“yield 对象可在后台任务中使用”这一行为,现在应当:
- 在后台任务函数内部自建资源(例如新建一个数据库会话),而不是复用
yield依赖里的会话; - 后台任务内部只使用不依赖
yield依赖资源的数据; - 不要把数据库对象直接作为参数传给后台任务函数,而是传对象的 ID,在后台任务内部用新会话重新查出该对象再处理。
3.7 演进时间线与迁移自查
综合原文档,带 yield 依赖的关键行为变更可按版本归纳为一张速查表,便于升级排查:
| 版本节点 | 变更内容 | 影响 |
|---|---|---|
| 0.106.0 | yield 之后可抛异常;退出代码执行时机调整为发送响应前/不再等后台任务 |
后台任务需自带资源,传入对象 ID 而非对象 |
| 0.110.0 | except 捕获后不重新抛出时,不再自动转发异常 |
需显式 raise 才能交给异常处理器 |
| 0.118.0 | 回退:退出代码改为响应发送完成后执行,修复 StreamingResponse 无法使用已关闭会话的问题 |
慢速流/长连接场景注意资源持有时间 |
| 0.121.0 | 新增 Depends(scope="function") 支持 |
可用 "function" 作用域获得“路径函数结束即清理”的精确控制 |
若要验证这些行为在当前版本代码库中的具体表现,可以参考对应测试用例,例如 tests/test_dependency_yield_scope.py 中针对 scope="function" / scope="request" 的退出代码顺序断言,以及 tests/test_dependency_yield_scope_websockets.py 中 WebSocket 场景下的等价覆盖。
四、小结
本文围绕 FastAPI 进阶依赖的两条主线完成了闭环:
- 可参数化依赖:通过
__init__携带配置 +__call__交由 FastAPI 解析执行,从而以“类的一个实例”作为Depends的依赖对象。这一模式不仅是定制校验逻辑的利器,更是 fastapi/security 中OAuth2PasswordBearer、HTTPBearer、API Key 等安全依赖的底层实现方式。 yield依赖生命周期:从 0.106.0、0.110.0、0.118.0 到 0.121.0 的多次演进,核心矛盾始终是“尽早释放资源”与“确保响应/后台任务仍能使用被释放资源”之间的平衡;当前推荐的落地方案是:默认使用scope="request",需要提前清理时用Depends(scope="function")或在依赖内部显式close()资源,并在后台任务中自建资源、只传 ID。
理解这两条主线,就能在编写高阶依赖注入逻辑时拥有清晰的心智模型,也能在升级旧版 FastAPI 应用时快速定位由依赖退出时机变化引发的问题。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00