FastAPI 进阶依赖指南:可参数化依赖(Callable 实例)与yield依赖生命周期演进详解
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
本篇技术指南围绕 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.2yield依赖与scope(0.121.0 起支持)
FastAPI0.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 yscope” 一节。
3.3yield依赖与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.5yield依赖与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 应用时快速定位由依赖退出时机变化引发的问题。
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考