FastAPI 教程:将 Python 类用作依赖(Class as Dependencies)——从 dict 到类型安全的依赖注入实践
依赖不一定非是函数不可:只要是一个 Python 可调用对象(callable),FastAPI 就会像解析路径操作函数的参数一样去解析它。本指南以本仓库官方教程文档 classes-as-dependencies.md 为主线,演示如何把前文依赖示例中的
dict返回改为一个携带__init__参数的类,从而让编辑器获得完整的代码补全与类型检查能力,并讲解Depends()无参简写背后的解析规则。读完你将掌握"类即依赖"的声明方式、类型注解与Depends各自承担的职责,以及 FastAPI 底层对 callable 的统一处理机制。
一个 dict 能做什么:从上一个示例说起
在依赖注入教程的上一部分(对应 tutorial001_an_py310.py),我们的依赖函数"dependable"返回的是一个 dict:
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
return {"q": q, "skip": skip, "limit": limit}
@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]):
return commons
@app.get("/users/")
async def read_users(commons: Annotated[dict, Depends(common_parameters)]):
return commons
于是 read_items / read_users 路径操作函数参数 commons 中拿到的就是一个 dict。问题随之而来:编辑器无法给 dict 提供太多支持(比如补全),因为它根本不知道这个字典里有哪些键、每个键的值是什么类型。commons["q"] 这种写法既没有补全提示,也无法做静态类型检查。
我们可以做得更好——用一个真正的 Python 类来替代。
什么构成一个依赖:关键在"可调用对象"
到目前为止,教程中出现的依赖都是用函数声明的:
async def common_parameters(...):
...
但函数不是声明依赖的唯一方式(虽然它可能是最常见的一种)。决定性的因素只有一个:依赖应当是一个"可调用对象"(callable)。
在 Python 里,"callable"指任何可以像函数一样被"调用"的东西。只要存在一个对象 something(它可能不是函数),而你能够这样执行它:
something()
或者:
something(some_argument, some_keyword_argument="foo")
那么 something 就是一个可调用对象。
类也是可调用对象
细想一下:创建一个 Python 类的实例,用的正是同一套调用语法:
class Cat:
def __init__(self, name: str):
self.name = name
fluffy = Cat(name="Mr Fluffy")
这里 fluffy 是 Cat 类的一个实例;而要制造出 fluffy,你正是在"调用" Cat。也就是说,Python 类天然就是一个可调用对象。
因此,在 FastAPI 中可以直接把 Python 类当作依赖来用。FastAPI 实际校验的只有两件事:
- 传进来的东西是否是一个"callable"(函数、类或任何其它可调用对象);
- 它声明了哪些参数。
如果你把一个 callable 作为依赖传给 FastAPI,FastAPI 会分析该 callable 的参数,并按与路径操作函数参数完全相同的方式去处理它们——包括嵌套子依赖。这个规则同样适用于完全没有参数的 callable,正如无参数的路径操作函数一样。
从源码看,FastAPI 在 fastapi/dependencies/utils.py 中的 get_dependant()(第 271 行起)与 analyze_param()(第 381 行起)完成这套解析:它们对 dependency 的 call 统一分析签名、提取子依赖并逐参数处理,而并不关心这个 callable 到底是 def 函数、class 还是别的可调用形式。get_dependant 中还会通过 _is_async_gen_callable、_is_gen_callable 等工具判定依赖属于普通协程、同步函数还是生成器变体,见 fastapi/dependencies/utils.py 对"parameter-less dependency must have a callable dependency"的断言。
用类声明依赖:__init__ 即依赖的参数
现在把上面的依赖 common_parameters 改写成类 CommonQueryParams。完整示例见 tutorial002_an_py310.py:
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
class CommonQueryParams:
def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
self.q = q
self.skip = skip
self.limit = limit
@app.get("/items/")
async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]):
response = {}
if commons.q:
response.update({"q": commons.q})
items = fake_items_db[commons.skip : commons.skip + commons.limit]
response.update({"items": items})
return response
请注意用来创建实例的 __init__ 方法:
def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
它的参数与之前的函数 common_parameters 完全一致。这些参数正是 FastAPI 用来"求解"依赖的输入,也就是说,FastAPI 会从 __init__ 的签名中提取查询参数的声明。两种写法最终得到的是完全相同的三件事:
- 一个可选的
q查询参数,类型为str; - 一个
skip查询参数,类型为int,默认值0; - 一个
limit查询参数,类型为int,默认值100。
两种写法下,这些数据都会被同样地转换类型、校验、写入 OpenAPI schema 文档,等等。唯一区别在于:函数版本把它们塞进一个 dict,类版本则把它们绑定为实例属性,进而在路径操作函数里可以直接通过 commons.q、commons.skip 访问。
使用这个类依赖
现在即可用这个类声明依赖:
@app.get("/items/")
async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]):
FastAPI 会"调用" CommonQueryParams 类、创建一个该类的实例,并把实例作为参数 commons 传入你的函数。__init__ 中写明的每个参数都会从请求中解析——q、skip、limit 从查询字符串而来,并在实例化前完成类型转换与校验。
这样,路由处理函数拿到的是一个具有真实类型 CommonQueryParams 的对象,编辑器能基于属性类型提供 commons.q、commons.skip、commons.limit 的补全,拼错属性名也会被静态检查捕获。
类型注解与 Depends 各司其职
请观察上面代码里 CommonQueryParams 出现了两次:
commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]
(Python 3.10+ 非 Annotated 写法为 commons: CommonQueryParams = Depends(CommonQueryParams);教程的 tip 建议尽可能使用 Annotated 版本。)
- 第二个
CommonQueryParams,即Depends(CommonQueryParams)里的那个,才是 FastAPI 真正用来识别依赖的:它会从这一个里提取声明的参数,最终也是调用它。 - 第一个
CommonQueryParams(类型注解位置的那个)对 FastAPI 没有任何特殊含义。FastAPI 不会用它做数据转换、校验等工作(那由Depends(CommonQueryParams)负责)。
因此,你甚至可以只写类型为 Any 的版本(见 tutorial003_an_py310.py):
from typing import Annotated, Any
from fastapi import Depends, FastAPI
app = FastAPI()
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
class CommonQueryParams:
def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
self.q = q
self.skip = skip
self.limit = limit
@app.get("/items/")
async def read_items(commons: Annotated[Any, Depends(CommonQueryParams)]):
response = {}
if commons.q:
response.update({"q": commons.q})
items = fake_items_db[commons.skip : commons.skip + commons.limit]
response.update({"items": items})
return response
(非 Annotated 版本对应 commons = Depends(CommonQueryParams)。)
但仍然强烈建议声明类型——这样做你的编辑器才能知道传入参数 commons 的是什么,进而给你提供代码补全与类型检查:
快捷方式:Depends() 免重复声明
上面的写法仍然有重复代码:CommonQueryParams 要手写两遍。
FastAPI 为这类依赖特指某个将被调用并创建实例的类的情况提供了一种快捷方式。与其写:
commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]
不如写:
commons: Annotated[CommonQueryParams, Depends()]
即把依赖声明为参数类型本身,并用不带任何参数的 Depends(),FastAPI 会从类型注解推断出该实例化哪个类。非 Annotated 版本对应 commons: CommonQueryParams = Depends()。
完整示例见 tutorial004_an_py310.py:
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]
class CommonQueryParams:
def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
self.q = q
self.skip = skip
self.limit = limit
@app.get("/items/")
async def read_items(commons: Annotated[CommonQueryParams, Depends()]):
response = {}
if commons.q:
response.update({"q": commons.q})
items = fake_items_db[commons.skip : commons.skip + commons.limit]
response.update({"items": items})
return response
如果这个技巧让你觉得反而更绕,那完全可以忽略它——它不是必须的,只是一种用来减少代码重复的简写。
需要注意它的适用边界:只有当依赖是"FastAPI 将会调用它来创建该类实例本身"的具体类时,Depends() 无参写法才成立。若依赖是函数、或者需要在 Depends(...) 里传入其它对象/参数,仍然需要显式写出依赖体。
运行验证:三种写法行为完全一致
本仓库的官方测试覆盖了这三种写法的等价性。在 tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py 中,tutorial002、tutorial003 与 tutorial004(含 _py310 与 _an_py310 变体)被统一收集为一个参数化的 client fixture,然后对同一组请求断言完全相同的响应。例如:
- 请求
/items(无参数)返回完整列表Foo、Bar、Baz; - 请求
/items?q=foo在响应中附带"q": "foo"; - 请求
/items?q=foo&skip=1从第二项开始返回切片; - 请求
/items?q=bar&limit=2只取前两条(可在该文件后续部分看到)。
测试文件第 10-25 行通过 pytest 的参数化 fixture 把这 6 个模块(tutorial002_py310、tutorial002_an_py310、tutorial003_py310、tutorial003_an_py310、tutorial004_py310、tutorial004_an_py310)放进同一套断言,从测试层面直接印证:函数式 dict 依赖、Annotated[Any, Depends(类)] 以及 Depends() 简写,解析出的查询参数与响应结构完全一致。
小结
围绕"类作为依赖"这一主题,可以提炼出四条可直接使用的经验:
- 依赖的本质是 callable。FastAPI 只关心"能不能调用"以及"参数是什么",函数、类乃至任意实现了
__call__的对象都能成为依赖。 - 类的
__init__参数就是依赖参数。把公共查询参数(或其它注入数据)放进__init__,FastAPI 会在每次请求时实例化该类并完成参数解析、转换、校验与 OpenAPI 文档化。 - 类型注解 ≠ 依赖体。
Depends(...)中传入的对象才决定依赖逻辑;类型注解仅供编辑器与静态检查使用,因此应尽量声明真实类型而非Any。 Depends()是"类即依赖"的简写。当依赖正是那个要被调用实例化的类时,可以省去在Depends里重复类名,但若依赖是函数等其它 callable,仍需显式书写。
相关文档与源码可继续查阅:依赖注入教程目录、本页依赖的核心解析实现 fastapi/dependencies/utils.py 以及等价性测试 test_tutorial002_tutorial003_tutorial004.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
