首页
/ FastAPI 路径操作装饰器中的依赖注入:用 `dependencies` 执行"只跑代码、不取返回值"的依赖

FastAPI 路径操作装饰器中的依赖注入:用 `dependencies` 执行"只跑代码、不取返回值"的依赖

2026-09-06 18:20:27作者:柯茵沙

本篇技术指南以 FastAPI 官方教程章节 Dependencies in path operation decorators 为骨架展开:当某个依赖仅需要被"求解/执行"、而不需要把返回值注入路径操作函数时,可以通过 @app.get(...) 等路径操作装饰器上的 dependencies=[Depends(...)] 参数一次性声明多个依赖。读完本文你将掌握该参数的完整用法、它与函数参数式 Depends 的差异、底层如何借助 _build_dependant_with_parameterless_dependencies 实现"无参数依赖",以及如何把同一套校验依赖复用到 APIRouter 分组和整个 FastAPI 应用。

适用场景:只要求依赖被执行,不需要它的返回值

在常规的依赖注入写法中,我们通过路径操作函数的参数声明依赖:

async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
    ...

依赖的返回值会通过 commons 注入到函数体内供业务逻辑使用。

但在两类场景下,你其实用不到依赖的返回值:

  • 路径操作函数内根本不需要该依赖的返回值;
  • 依赖本身设计上就不返回任何值(典型的如"校验请求头是否合法"这类守卫型依赖)。

你依然希望这个依赖被调度、被执行——例如某些请求头不满足要求时,它会在业务逻辑运行之前抛出 HTTPException 拦截请求。

FastAPI 官方教程(docs/en/docs/tutorial/dependencies/dependencies-in-path-operation-decorators.md)给出的结论是:这种情况下,不要再给路径操作函数声明参数,而是把 Depends(...) 写进路径操作装饰器的 dependencies 参数里。依赖仍会以与普通依赖完全相同的方式被执行/求解,但它的返回值不会传给路径操作函数。

给路径操作装饰器添加 dependencies:核心用法

路径操作装饰器接收一个可选参数 dependencies,它的取值是一个由 Depends() 组成的 list。官方示例保存于 docs_src/dependencies/tutorial006_an_py310.py(使用 Annotated 语法的版本):

from typing import Annotated

from fastapi import Depends, FastAPI, Header, HTTPException

app = FastAPI()


async def verify_token(x_token: Annotated[str, Header()]):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def verify_key(x_key: Annotated[str, Header()]):
    if x_key != "fake-super-secret-key":
        raise HTTPException(status_code=400, detail="X-Key header invalid")
    return x_key


@app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
    return [{"item": "Foo"}, {"item": "Bar"}]

这段代码展示了两点关键事实:

  1. dependencies 声明在装饰器上,路径操作函数 read_items() 自身没有任何参数;
  2. verify_token 不返回值,verify_key 返回了 x_key——但返回值并不会被使用。只要它们在列表里,就一定会先于路径操作函数执行;任何一个抛出的 HTTPException 都会直接以 400 状态码响应给客户端,业务函数根本不会执行。

仓库还提供了对应的默认值写法版本 docs_src/dependencies/tutorial006_py310.py,即把 x_token: str = Header() 写在函数签名默认值里,两种写法行为一致。

提示:部分编辑器/静态工具会检查未使用的函数参数并标红。把这类依赖移到装饰器上,既能保证它们被执行,又能避免编辑器与工具链的误报;对刚接触项目的新人来说,也避免了"这个参数怎么没用"的困惑。

注意:示例中的 X-KeyX-Token 是教学用的自定义请求头。真正实现认证/鉴权时,更推荐使用内置的 Security 安全工具集(即本教程的下一章),它提供了基于 OpenAPI 规范的 OAuth2、APIKey、HTTP Bearer 等标准方案。

底层原理:依赖如何变成"无参数子依赖"并被执行

参数被保存并构建为子依赖

在路径操作(例如 @app.get(...))注册阶段,装饰器的 dependencies 会被保存并解析。fastapi/routing.pyAPIRoute 的初始化逻辑接收该参数:

  • 参数签名 dependencies: Sequence[params.Depends] | None = None(见 fastapi/routing.py);
  • 保存为 self.dependencies = list(dependencies or [])fastapi/routing.py)。

随后在构建请求处理链时调用 _build_dependant_with_parameterless_dependenciesfastapi/routing.py),其核心逻辑是:

dependant = get_dependant(path=path, call=call, scope="function")
for depends in dependencies[::-1]:
    dependant.dependencies.insert(
        0,
        get_parameterless_sub_dependant(depends=depends, path=path),
    )

这里有两个值得注意的实现细节:

  • 每个 Depends(verify_token) 都被包装成一个无参数子依赖(parameterless sub-dependant),通过 get_parameterless_sub_dependantfastapi/dependencies/utils.py)为其构建独立的 Dependant 对象;
  • 代码逆序遍历并始终插入索引 0,最终使子依赖在列表中的顺序与声明顺序一致。

"无参数"正是"返回值不注入"的关键:子依赖照常求解,但由于没有路径操作函数参数去接收它,求解出的值不会出现在 values 中。

请求到达时统一求解

请求命中该路径时,FastAPI 调用 solve_dependenciesfastapi/dependencies/utils.py)统一解析依赖图。它会遍历 dependant.dependencies,对每个子依赖递归求解:先解析子依赖自己声明的要求(如下面的 Header()),再执行依赖函数本身。

由实现可见,装饰器级依赖与函数参数级依赖走的是同一条求解管线,因此它们拥有完全一致的能力:可以声明请求头/请求体/查询参数等要求,可以声明自己的子依赖,可以 raise 异常,也可以返回任何值(返回值仅被求解、不会被业务函数消费)。当函数通过 app.dependency_overrides 被覆盖时(对应 dependency_overrides_provider 分支),装饰器级依赖同样会被替换解析。此外,子依赖默认开启 use_cache(缓存键见 fastapi/dependencies/utils.py 附近的命中判断),保证同一请求内同一个依赖函数只被真正调用一次。

依赖的能力边界:请求要求、异常与返回值

官方教程用一个专门小节归纳了装饰器级依赖的能力范围。

可以声明请求要求与子依赖

依赖函数内部可以像普通依赖一样声明请求要求(如 Header()),也可以继续嵌套 Depends 声明子依赖,求解是递归展开的:

async def verify_token(x_token: Annotated[str, Header()]):
    ...

x_token 是必填字符串请求头——请求缺少该头时,会像普通参数校验一样返回 422 校验错误,而不是 200。

可以抛出异常

依赖函数可以像普通依赖一样 raise 异常。示例中两个校验函数都以 HTTPException(status_code=400, ...) 失败退出,直接终止该请求的处理。

返回值可用可不用,但依赖一定会执行

verify_key 明明返回了 x_key,但它写在装饰器 dependencies 里,返回值就没有任何消费者。这正是该特性的设计意图:你可以把一个已经在别处返回值的普通依赖原样复用到这里,即便这里用不到它的返回值,依赖也照样会被执行。这在把同一套鉴权/校验逻辑"多点复用"时非常有用——不必为装饰器场景专门编写一个不返回值的孪生函数。

下表总结了两种声明方式的差异:

对比维度 函数参数式 Depends 装饰器 dependencies
声明位置 路径操作函数参数 @app.<method> 装饰器
依赖是否执行
返回值是否注入 注入到参数 不注入(无消费者)
典型用途 注入查询参数、DB session 等业务值 纯守卫型校验、执行副作用
编辑器未用参数告警 可能触发 不触发
报错/校验行为 与普通依赖完全一致 与普通依赖完全一致

用仓库测试验证行为边界

官方对这两个代码文件各写了一套端到端测试,见 tests/test_tutorial/test_dependencies/test_tutorial006.py。它通过 pytest 参数化同时覆盖 tutorial006_py310tutorial006_an_py310 两个变体,从断言中可以确认:

  • 无请求头GET /items/ 返回 422,detail 中同时包含两条错误:x-tokenx-keymissingField required)——证明装饰器级依赖声明的必填请求头同样走校验;
  • Token 错误:只带 X-Token: invalid 返回 400,内容为 {"detail": "X-Token header invalid"}
  • Key 错误:Token 正确但 X-Key 错误时返回 400,内容为 {"detail": "X-Key header invalid"}
  • 全部正确:两个请求头都正确时返回 200 与 [{"item": "Foo"}, {"item": "Bar"}]
  • OpenAPI 可见性test_openapi_schema 断言 openapi.json 中该路径的 parameters 包含 x-tokenx-key 两个必填(required: True)请求头参数,顺序与声明顺序一致。也就是说,装饰器级依赖的要求会被如实反映到 /docs 交互文档中,客户端调用者能够看到"这个接口需要带哪些头"。

这些测试对学习者是很有价值的"可运行证据":你可以把上述任一源码文件保存为 main.py,用 uvicorn main:app --reload(或通过仓库内置入口 python -m fastapi dev main.py,见 fastapi/main.py)启动后,用 curl 依次复现 422 / 400 / 200 三种响应。

扩展:把同一组依赖应用到一批路径操作与应用全局

路由器级别:APIRouter 的 dependencies

官方教程提示,当你把应用拆成多文件、使用 APIRouter 组织"更大应用"时(见 Bigger Applications - Multiple Files),可以把 dependencies 声明在路由器的构造参数上,让该路由器下所有路径操作共享这批依赖:

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(verify_token), Depends(verify_key)],
)

APIRouter.__init__ 同样接收 dependencies: Sequence[params.Depends] | None = Nonefastapi/routing.py),被 include_router 纳入时会与父级依赖合并,从源码结构看(fastapi/routing.py 一带的合并逻辑)子路由与父路由的依赖会按父在前、子在后的顺序拼装,最终统一参与请求求解。这样你只需写一次校验函数,就能让整组接口都具备相同的进入门槛。

应用级别:FastAPI 全局依赖

再往上,可以把 dependencies 传给 FastAPI 应用本身,让应用内的每一个路径操作都执行它们。仓库配套的全局依赖教程见 Global Dependencies,其示例代码在 docs_src/dependencies/tutorial012_an_py310.py

app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)])


@app.get("/items/")
async def read_items():
    return [{"item": "Portal Gun"}, {"item": "Plumbus"}]


@app.get("/users/")
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]

此时 verify_tokenverify_key 会作用于 /items//users/ 乃至后续添加的所有路径操作,但同样不向任何路径操作函数传递返回值。若你只想对部分接口生效,则退回到路由器级或本文讲解的"路径操作装饰器级"即可——三者共享同一套 Depends 语法与求解机制,只是作用范围从"单个接口"放大到"一组接口"再到"整个应用"。

小结

  • 当依赖只需要被执行、返回值在业务逻辑中用不到时,把它放进路径操作装饰器的 dependencies=[Depends(...), ...] 列表,而不是声明为函数参数;
  • 装饰器级依赖与普通依赖行为完全一致:能声明请求要求与子依赖、能 raise 异常拦截请求、返回值可有可无(一律不会被注入);
  • 其实现原理是把每个 Depends 包装成"无参数子依赖"(get_parameterless_sub_dependant),再通过统一的 solve_dependencies 求解管线解析,见 fastapi/routing.pyfastapi/dependencies/utils.py
  • 同一写法可平移到 APIRouterFastAPI(...) 应用级别,实现分组或全局的横切校验。

继续阅读官方教程时,可按目录顺序进入 Global Dependencies(如何做应用级全局依赖),随后在 Bigger Applications - Multiple Files 中掌握多文件项目的依赖组织方式。

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