FastAPI 教程:将 Python 类用作依赖(Class as Dependencies)——从 dict 到类型安全的依赖注入实践
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
依赖不一定非是函数不可:只要是一个 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变体)被统一收集为一个参数化的clientfixture,然后对同一组请求断言完全相同的响应。例如:
- 请求
/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。
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考