FastAPI 依赖注入进阶:子依赖(Sub-dependencies)图解析与 use_cache 缓存机制实战
FastAPI 的依赖注入系统允许依赖函数再次声明自己的依赖,从而形成任意深度的"依赖图"。本文以依赖注入教程中的子依赖章节为核心,结合仓库中的源码实现,完整讲解如何构建"query_extractor → query_or_cookie_extractor → 路径操作"这样一条多级依赖调用链,并深入剖析同一次请求中重复依赖的缓存语义与 use_cache=False 的真实行为,帮助你写出层次清晰、可复用的真实业务代码。
什么是子依赖(Sub-dependencies)
在 FastAPI 中,依赖不一定是"一层到底"的。你可以创建本身还声明了其他依赖的依赖函数,这就是子依赖(sub-dependencies)。只要嵌套得当,这种结构可以做得多深都行,而 FastAPI 会自动负责解析整棵依赖树,你不必手动关心调用顺序。
用官方术语来说:
- dependable:作为"被依赖方"存在的依赖函数;
- dependant:声明了自身依赖的调用方。
而一个函数可以同时扮演两个角色——它既可以是别人的 dependable,也可以是某个依赖的 dependant。这正是子依赖体系的精髓所在。
第一步:定义一个最基础的 "dependable"
先从最简单的依赖函数开始。它只声明了一个可选的查询参数 q,然后原样把它返回(源码见 docs_src/dependencies/tutorial005_an_py310.py):
def query_extractor(q: str | None = None):
return q
这个函数本身非常简单,甚至"没什么用",但它是理解子依赖如何工作的理想起点:依赖函数和路径操作函数长得完全一样——同样可以声明路径参数、查询参数、请求体、Cookie 等,FastAPI 会一视同仁地做校验与注入。
第二步:一个同时是 "dependable" 和 "dependant" 的依赖
接着,创建第二个依赖函数。它既是一个独立的依赖(dependable),又在内部声明了对 query_extractor 的依赖,因此它同时也是一个 dependant(见 tutorial005_an_py310.py):
def query_or_cookie_extractor(
q: Annotated[str, Depends(query_extractor)],
last_query: Annotated[str | None, Cookie()] = None,
):
if not q:
return last_query
return q
拆解它的两个参数声明:
q: Annotated[str, Depends(query_extractor)]:该函数依赖query_extractor,并把它的返回值绑定到参数q。FastAPI 会保证在调用query_or_cookie_extractor之前,先调用query_extractor并把结果传入。last_query: Annotated[str | None, Cookie()] = None:声明一个可选的字符串 Cookie。业务逻辑是:当用户没有在请求里提供q查询参数时,就回退使用之前保存在 Cookie 里的"上一次查询"。
这就是一个完整的"兜底查询"场景——查询参数优先,Cookie 次之。
如果你不使用 Annotated 语法(Python 3.10+ 但偏好默认值写法),等价版本见 tutorial005_py310.py,效果完全相同:
def query_or_cookie_extractor(
q: str = Depends(query_extractor), last_query: str | None = Cookie(default=None)
):
if not q:
return last_query
return q
提示:官方文档建议优先使用
Annotated版本,写法更统一、可读性更好。
第三步:在路径操作中只声明最外层的依赖
路径操作函数里只需声明一个依赖,也就是最外层的 query_or_cookie_extractor:
@app.get("/items/")
async def read_query(
query_or_default: Annotated[str, Depends(query_or_cookie_extractor)],
):
return {"q_or_cookie": query_or_default}
注意一个关键点:你完全没有手动声明 query_extractor。但 FastAPI 会从 query_or_cookie_extractor 的签名中"读懂"它还需要 query_extractor,于是自动先行求解 query_extractor,再把结果喂给 query_or_cookie_extractor。
整条调用链可以用依赖图表示:
graph TB
query_extractor(["query_extractor"])
query_or_cookie_extractor(["query_or_cookie_extractor"])
read_query["/items/"]
query_extractor --> query_or_cookie_extractor --> read_query
从源码看 FastAPI 如何构建依赖树
这套"自动求解"在底层发生在 fastapi/dependencies/utils.py 的 get_dependant() 函数中。当 FastAPI 为一个路径操作构建 Dependant 对象时,会遍历目标函数的签名参数;一旦发现某个参数被 Depends 包裹(param_details.depends is not None),就会递归地对该依赖函数再调用一次 get_dependant(),生成一个子 Dependant 并挂到父节点的 dependrant.dependencies 列表中(utils.py):
sub_dependant = get_dependant(
path=path,
call=param_details.depends.dependency,
name=param_name,
...
use_cache=param_details.depends.use_cache,
)
dependant.dependencies.append(sub_dependant)
可以看到每个 Depends 上的 use_cache 标志会随递归一路透传下去,最终在真正求解请求时决定该子依赖的返回值是否走缓存。这也就是为什么你可以声明任意深度的嵌套依赖——递归构建保证了整棵树在请求到来之前就已经被完整地"铺开"。
同一依赖被多次声明:默认只调用一次(缓存机制)
真实项目里很容易出现这样的场景:多个依赖都共享同一个子依赖。例如 A 和 B 都依赖 common,而路径操作又同时依赖 A 和 B。
如果在每个地方都显式声明一遍 common,直觉上它似乎会被调用两次。但 FastAPI 知道在同一路径操作中,对每个 request 只会调用该子依赖一次:
- 首次求解时返回的值会被保存在一个请求级别的缓存(cache) 中;
- 之后所有需要该值的 "dependants" 都会直接复用这个缓存值,而不会再执行一次依赖函数。
官方文档对 "cache" 的定义是:一种用来存储已计算/已生成值的工具系统,以便复用而不是重复计算。这一点在源码中也有印证——fastapi/dependencies/utils.py 的求解逻辑里,只有当 sub_dependant.use_cache 为真且缓存键命中时才会复用已算出的结果。
这一默认行为不仅省掉了重复的 I/O(比如重复查库、重复请求外部服务),还保证了同一请求内依赖实例的状态一致性。
高级场景:use_cache=False 强制每次重新求值
默认缓存语义对绝大多数情况都是最优的,但存在少数高级场景:你明确希望该依赖在同一请求的每一步、每出现一次都被真实调用,而不是复用缓存值。此时可以把 Depends 的 use_cache 参数设为 False。
Annotated 写法(Python 3.10+):
async def needy_dependency(fresh_value: Annotated[str, Depends(get_value, use_cache=False)]):
return {"fresh_value": fresh_value}
非 Annotated 写法:
async def needy_dependency(fresh_value: str = Depends(get_value, use_cache=False)):
return {"fresh_value": fresh_value}
提示:能使用
Annotated时优先使用Annotated版本。
从源码看,use_cache 是 Depends 这个不可变数据类的字段之一(见 fastapi/params.py):
@dataclass(frozen=True)
class Depends:
dependency: Callable[..., Any] | None = None
use_cache: bool = True
scope: Literal["function", "request"] | None = None
默认值为 True。此外 Security 是继承自 Depends 的子类(fastapi/params.py),因此同样的 use_cache 语义也适用于安全相关的依赖声明;而通过 fastapi.param_functions 中的 Depends() / Security() 工厂函数(fastapi/param_functions.py、fastapi/param_functions.py),该参数会原样透传到 params.Depends 上。
典型适用场景包括:某个依赖会基于当前时刻或随机数产生结果,你希望在同一个请求中每次依赖被解析时都拿到"新鲜值",而测试中也可能用它来精确断言依赖被调用的次数。
小结:依赖注入本质上是"函数图"
抛开各种花哨的术语,FastAPI 的依赖注入系统其实非常简单:
- 依赖就是普通函数,写法和路径操作函数一模一样;
- 它们之间可以通过
Depends自由嵌套,组成任意深度、任意形状的依赖树/图; - FastAPI 负责递归求解整张图,并对同一请求内重复出现的子依赖做默认缓存,同时开放
use_cache=False作为逃生舱门。
虽然上面的例子看起来简单,甚至有些"小题大做",但当进入安全(Security)相关章节时你会发现:OAuth2、JWT 校验、权限范围(scopes)、数据库会话这类逻辑都适合被抽象成子依赖,复用的收益会非常可观——大量重复代码会被一张张清晰的依赖图取代。
延伸阅读
如果你想继续深入这套依赖体系,仓库中的同系列文档可供对照学习:
- 依赖注入入门:docs/en/docs/tutorial/dependencies/index.md
- 用类作为依赖:docs/en/docs/tutorial/dependencies/classes-as-dependencies.md
- 路径操作装饰器中的依赖:docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md
- 全局依赖:docs/en/docs/tutorial/dependencies/global-dependencies.md
- yield 型依赖与清理逻辑:docs/en/docs/tutorial/dependencies/dependencies-with-yield.md
- 依赖注入的完整源码实现(递归构建依赖树、缓存求解):fastapi/dependencies/utils.py
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