首页
/ FastAPI 依赖注入进阶:子依赖(Sub-dependencies)图解析与 use_cache 缓存机制实战

FastAPI 依赖注入进阶:子依赖(Sub-dependencies)图解析与 use_cache 缓存机制实战

2026-09-07 09:34:44作者:宣聪麟

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,并把它的返回值绑定到参数 qFastAPI 会保证在调用 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.pyget_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 标志会随递归一路透传下去,最终在真正求解请求时决定该子依赖的返回值是否走缓存。这也就是为什么你可以声明任意深度的嵌套依赖——递归构建保证了整棵树在请求到来之前就已经被完整地"铺开"。

同一依赖被多次声明:默认只调用一次(缓存机制)

真实项目里很容易出现这样的场景:多个依赖都共享同一个子依赖。例如 AB 都依赖 common,而路径操作又同时依赖 AB

如果在每个地方都显式声明一遍 common,直觉上它似乎会被调用两次。但 FastAPI 知道在同一路径操作中,对每个 request 只会调用该子依赖一次

  • 首次求解时返回的值会被保存在一个请求级别的缓存(cache) 中;
  • 之后所有需要该值的 "dependants" 都会直接复用这个缓存值,而不会再执行一次依赖函数。

官方文档对 "cache" 的定义是:一种用来存储已计算/已生成值的工具系统,以便复用而不是重复计算。这一点在源码中也有印证——fastapi/dependencies/utils.py 的求解逻辑里,只有当 sub_dependant.use_cache 为真且缓存键命中时才会复用已算出的结果。

这一默认行为不仅省掉了重复的 I/O(比如重复查库、重复请求外部服务),还保证了同一请求内依赖实例的状态一致性。

高级场景:use_cache=False 强制每次重新求值

默认缓存语义对绝大多数情况都是最优的,但存在少数高级场景:你明确希望该依赖在同一请求的每一步、每出现一次都被真实调用,而不是复用缓存值。此时可以把 Dependsuse_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_cacheDepends 这个不可变数据类的字段之一(见 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.pyfastapi/param_functions.py),该参数会原样透传到 params.Depends 上。

典型适用场景包括:某个依赖会基于当前时刻或随机数产生结果,你希望在同一个请求中每次依赖被解析时都拿到"新鲜值",而测试中也可能用它来精确断言依赖被调用的次数。

小结:依赖注入本质上是"函数图"

抛开各种花哨的术语,FastAPI 的依赖注入系统其实非常简单:

  • 依赖就是普通函数,写法和路径操作函数一模一样;
  • 它们之间可以通过 Depends 自由嵌套,组成任意深度、任意形状的依赖树/图
  • FastAPI 负责递归求解整张图,并对同一请求内重复出现的子依赖做默认缓存,同时开放 use_cache=False 作为逃生舱门。

虽然上面的例子看起来简单,甚至有些"小题大做",但当进入安全(Security)相关章节时你会发现:OAuth2、JWT 校验、权限范围(scopes)、数据库会话这类逻辑都适合被抽象成子依赖,复用的收益会非常可观——大量重复代码会被一张张清晰的依赖图取代。

延伸阅读

如果你想继续深入这套依赖体系,仓库中的同系列文档可供对照学习:

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388