10 依赖注入 —— 让你的 handler 只说"我要什么",不管"怎么来的"
前置阅读: 04-路由、09-中间件
关键词: Depends, yield 依赖, async def 生成器, 资源清理
难度: ★★★☆☆
场景导入:当 handler 的参数列表比它的业务逻辑还长
一个典型的图书列表接口需要哪些上下文?数据库会话、当前用户信息、分页参数、请求 ID、限流配额……如果每个 handler 都自己从 Request 对象里翻找和组装这些公共数据,参数列表会比函数体还长,而且业务逻辑和上下文解析搅成一团。
FastAPI 的答案是依赖注入(Dependency Injection)。它的核心理念很简单:handler 在函数签名里声明"我需要什么",框架在调用 handler 之前按依赖图逐层解析出这些对象,注入到对应参数上。handler 不关心 AsyncSession 是怎么创建的、current_user 是从哪个 token 解析的——它只消费结果。
更巧妙的是 yield 依赖形态。它把资源的"准备"和"清理"写进同一个函数——yield 之前做初始化,yield 之后做回收——框架保证即使 handler 抛出异常,清理代码也会执行。本章以一个虚构的"请求计时器"作为载体,把 Depends、yield、嵌套依赖三种形态讲透。数据库相关的依赖从第 13 章开始登场。
原理解析:从声明到注入的完整链路
依赖注入在 FastAPI 中不是"魔法",而是一条明确的解析管线。启动期,框架分析每个 handler 的函数签名,把 Depends(...) 标记的参数登记到依赖图。请求期,solve_dependencies 按拓扑序解析依赖树,把结果以关键字参数注入 handler。
(图注:solve_dependencies 串联依赖调用。yield 依赖在响应返回前完成清理,异常路径也受同一机制保护。)
Depends 是声明依赖的入口。它可以接收同步函数、async def 协程、甚至是一个类——类的 __init__ 签名会被递归解析,其参数本身也可以来自 Depends。当依赖内部需要 await 时(如计时、异步锁、令牌校验),写成 async def;纯计算或一次性内存对象用普通 def 即可。
yield 依赖是 FastAPI 0.95 之后引入的形态,本质上是一个被框架包装为上下文管理器的生成器。yield 之前的代码相当于 __aenter__(资源准备),yield 把值注入到 handler,响应发送后回到 yield 之后执行 __aexit__(资源清理)。在异步栈下推荐写成 async def 生成器,返回类型标注 AsyncIterator[T]——这样既能 await 异步资源(连接池借出、令牌桶获取),又能在响应后保证回收。
嵌套依赖是依赖图自然支持的特性。get_request_context 依赖 get_request_id 和 get_timer,get_viewer 又依赖 get_request_context——形成一棵解析树。框架按拓扑序执行,被多个上层依赖共享的子依赖只解析一次,同一请求内结果缓存复用:
(图注:依赖按声明顺序串联解析,子依赖结果向上回传;yield 依赖的清理在响应后执行,与正常退出和异常路径保持一致。)
依赖注入的设计动机有两个核心点。声明式协作:handler 签名直接说明它需要哪些对象,IDE 和 OpenAPI 文档据此生成精确信息。测试便利:测试时可以通过 app.dependency_overrides 把真实依赖替换为测试替身,handler 代码零改动即可在无网络、无数据库的环境下运行。
代码实现:用计时器演示 yield 依赖的完整生命周期
本章刻意避开数据库,用一个虚构的"请求计时器"作为 async def yield 依赖的载体。数据库相关的依赖(get_async_db 等)从第 13 章开始引入。
# app/deps/timer.py
from collections.abc import AsyncIterator
import time
async def timer_dep() -> AsyncIterator[float]:
"""async def 生成器:yield 起始时间,响应返回后输出耗时。
yield 之前相当于 __aenter__,yield 之后相当于 __aexit__。
框架保证即使 handler 抛异常,清理逻辑也会执行。
"""
start = time.perf_counter()
yield start
duration = time.perf_counter() - start
print(f"[timer] {duration * 1000:.2f} ms")
timer_dep 的函数签名使用 AsyncIterator[float](或 AsyncGenerator[float, None]),FastAPI 通过这个返回类型识别为异步 yield 依赖。yield start 把起始时间注入到 handler;响应返回后,执行权回到 yield 之后,打印实际耗时。即使 handler 中途抛异常,框架也会通过 AsyncExitStack 保证清理代码执行——这一点与 try/finally 的语义一致。
# app/api/health.py
from typing import Annotated
from fastapi import APIRouter, Depends
from app.deps.timer import timer_dep
router = APIRouter(tags=["health"])
@router.get("/ping")
async def ping(start: Annotated[float, Depends(timer_dep)]) -> dict:
"""async def handler + async def yield 依赖,无数据库。
响应发送后,timer_dep 会回到 yield 之后的清理逻辑,
打印出本次请求的实际耗时。
"""
return {"pong": True}
Annotated[float, Depends(timer_dep)] 是 FastAPI 推荐的类型化依赖写法——把"类型"和"依赖来源"绑定到一个别名,handler 签名只保留一个简洁的 start 形参。在更低 Python 版本中也可以用 start: float = Depends(timer_dep),但 Annotated 风格对 IDE 和类型检查器更友好。
避坑指南
yield 之后的代码只做轻收尾。清理资源、打点计时、关闭连接——这些是 yield 之后的本职工作。但如果把可能抛异常的业务逻辑(如写审计日志)也放在 yield 之后,一旦它失败,清理异常会覆盖 handler 里原本的业务异常,让排查变得困难。
嵌套依赖层数控制在三层以内。过深的依赖树让请求期调试变得痛苦——你在 handler 里看到的只是一个参数名,却不知道它背后串了多少层。建议把深嵌套依赖拆成多层模块(如
app/deps/auth、app/deps/db),模块级 docstring 说明解析顺序。子依赖在同一层级共享缓存。这个特性是性能优化,也是设计约束——如果你期望每次调用
Depends(get_db)都拿到不同的 Session,那依赖注入的缓存机制可能不符合预期。本系列每个请求通过async def生成器 yield 一个新的AsyncSession,缓存的是同一个请求内的同一个 Session 实例,这符合"一次请求一个 Session"的语义。
面试 QA
Q1 [原理]: FastAPI 的 yield 依赖语义是什么?为什么异步栈下推荐 async def 生成器?
yield 依赖是 FastAPI 对"资源生命周期管理"的内置方案。yield 把函数体分成两个阶段:yield 之前做资源准备并将值注入 handler,yield 之后在响应返回前执行清理。框架内部把它包装成 __aenter__ / __aexit__ 协议,通过 AsyncExitStack 确保异常路径也走清理逻辑。
异步栈下推荐 async def 生成器,因为这种形态既能 await 异步资源(连接池借出、令牌桶获取),又能用 AsyncIterator[T] 类型注解清晰表达意图。同步 yield 依赖(def + Iterator[T])只能管理纯内存资源,无法在 yield 之前做异步握手——这在需要 await 的数据库连接池场景下根本不够用。
Q2 [项目]: 依赖嵌套的最佳实践是什么?
三条原则。其一,把深嵌套依赖拆到独立模块,模块级 docstring 画出依赖解析顺序,关键路径层数控制在三层以内。其二,利用框架的子依赖缓存机制——被多个上层依赖共享的子依赖只解析一次,所以"获取配置""解析 token"这类通用依赖应该抽出来复用,而不是在每个上层依赖里重复声明。其三,yield 依赖的清理逻辑只做资源关闭和计时打点,不要在 yield 之后做可能抛异常的业务操作。
小结
本章用 Depends 和 async def yield 依赖演示了依赖注入的完整生命周期:yield 之前准备资源,yield 把值注入 handler,响应发送后回到 yield 之后清理——异常路径也受 AsyncExitStack 保护。数据库相关的依赖(get_async_db 等)本章刻意不引入,把"依赖注入的通用机制"和"数据库访问的具体实现"分开讲解,避免读者把 Depends 当成 ORM 专属语法。
下一篇《11 中间件与依赖注入的区别》将用一张全表对比两种机制,给你一个清晰的选型决策框架:什么时候走中间件,什么时候用依赖。