FastAPI 依赖注入进阶:使用 Python 类作为依赖(Classes as Dependencies)完整指南
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
在 FastAPI 的依赖注入体系中,依赖并不局限于函数——任何"可调用(callable)"对象都可以充当依赖,其中最常见的形态就是把一个 Python 类直接作为依赖使用。本文将基于官方教程讲解如何用类替代函数型依赖、__init__参数如何被自动解析、Depends()无参快捷写法背后的原理,并结合当前仓库的源码与测试,深入剖析"类即依赖"的实现机制。
从dict依赖说起:为什么我们"可以做得更好"
在引入类依赖之前,教程中的依赖通常是一个返回dict的异步函数,例如 docs_src/dependencies/tutorial001_an_py310.py:
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这样做的结果是,路径操作函数的commons参数拿到的只是一个普通的dict。问题随之而来:IDE 无法对dict提供自动补全与类型检查——编辑器不知道字典里有哪些键、每个键是什么类型。于是我们自然想到:能不能让依赖返回一个"有类型"的对象?
什么才是一个"依赖"?核心判定标准是 callable
到目前为止,我们见过的依赖都是函数,但函数并不是唯一的依赖形式。判断一个对象能否成为 FastAPI 依赖的关键只有一条:
依赖必须是"可调用(callable)"的。
在 Python 中,"可调用"指任何可以像函数一样被"调用(执行)"的对象,例如:
something()something(some_argument, some_keyword_argument="foo")只要满足这种调用语法,无论它是一个函数、一个类,还是实现了__call__的实例,都属于 callable。
而创建类的实例恰恰使用的就是这套语法:
class Cat: def __init__(self, name: str): self.name = name fluffy = Cat(name="Mr Fluffy")这里fluffy是Cat的实例,而创建它的过程本质上是"调用"了Cat这个类。类因此天然是 callable,也就天然具备成为 FastAPI 依赖的资格。
FastAPI 实际校验的,就是依赖是否为一个 callable,以及它所声明的参数(参见 fastapi/dependencies/utils.py 中get_parameterless_sub_dependant对callable(depends.dependency)的断言)。只要传入的是 callable,框架就会分析其参数并按照与路径操作函数参数完全相同的方式处理——包括类型转换、校验、OpenAPI 文档生成以及子依赖的解析。
用类重写依赖:CommonQueryParams
现在我们把函数型依赖common_parameters改写成类CommonQueryParams,完整示例见 docs_src/dependencies/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__方法的签名——它与之前的common_parameters函数参数完全一致:
def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):FastAPI 正是依靠__init__的参数来"求解(solve)"依赖:它会把每个参数当作查询参数来处理(self除外)。在函数版与类版两种写法下,最终效果完全相同:
| 查询参数 | 类型 | 默认值 | 是否必填 |
|---|---|---|---|
q | str | None | 可选 |
skip | int | 0 | 可选 |
limit | int | 100 | 可选 |
无论是函数版还是类版,这些数据都会被转换类型、校验、并写入 OpenAPI 架构。这一点在仓库测试中有直接证据:tests/test_tutorial/test_dependencies/test_tutorial002_tutorial003_tutorial004.py 中的test_openapi_schema断言了/openapi.json中q、skip、limit三个查询参数(含title、default、type等字段)与默认值0、100的完整生成结果。
使用类依赖:实例注入而非类本身
声明好类依赖后,在路径操作函数中这样使用:
@app.get("/items/") async def read_items(commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]):当请求到达时,FastAPI 会"调用"CommonQueryParams这个类,创建它的实例,并把实例作为commons的值传入你的函数。所以函数体里访问的是commons.q、commons.skip、commons.limit这些实例属性,而不是字典键。
路由函数中基于实例属性实现的分页逻辑:
items = fake_items_db[commons.skip : commons.skip + commons.limit]依赖注入与校验、分页、响应组装协同工作,最终返回如下的 JSON:
{"items": [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]}带查询参数请求/items?q=bar&skip=1&limit=1时则返回{"items": [{"item_name": "Bar"}], "q": "bar"}——这些行为均被上述测试文件的test_get用例逐一验证。
类型注解 vsDepends:同一个类写了两次
细心的读者会发现CommonQueryParams在参数声明中出现了两次:
commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)]第二次出现(Depends(CommonQueryParams)内部的那个)才是 FastAPI 真正使用的依赖定义——框架从这里提取依赖参数,并实际"调用"它。
第一次出现(类型注解位置的CommonQueryParams)对 FastAPI 本身没有特殊意义,它不参与数据转换与校验。也就是说,你甚至可以写成这样(见 docs_src/dependencies/tutorial003_an_py310.py):
from typing import Annotated, Any # ... @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虽然功能上完全等价,但强烈建议保留类型注解:它是你的编辑器判断commons具体类型、提供自动补全与类型检查的唯一依据。下图中可以看到,当代码写作commons: CommonQueryParams = Depends(CommonQueryParams)时,IDE 能识别出commons.skip的类型为int并给出提示:
快捷写法:Depends()无参调用
上面的写法中CommonQueryParams被写了两次,存在明显的代码重复。针对**"依赖特指某个类、且 FastAPI 将调用该类创建实例"**这种场景,FastAPI 提供了快捷写法:把类写在类型注解位置,Depends()括号内留空,完整示例见 docs_src/dependencies/tutorial004_an_py310.py:
@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 responseFastAPI 会从类型注解中"读出"CommonQueryParams,然后自动完成依赖的解析与调用。这个能力从源码结构看,源于 fastapi/params.py 中Depends的定义——它的dependency字段默认为None:
@dataclass(frozen=True) class Depends: dependency: Callable[..., Any] | None = None use_cache: bool = True scope: Literal["function", "request"] | None = None当dependency为空时,框架会回退到参数的类型注解来定位实际的依赖类(即上文get_parameterless_sub_dependant所处理的"无参依赖"路径)。use_cache与scope字段则分别控制依赖结果的缓存行为与作用域("function"或"request")。
提示:如果这种快捷写法让你觉得比之前更困惑,完全可以不用——它只是一个减少代码重复的语法糖,并不是必需的。
两种写法与 Python 版本适配
本文示例均基于 Python 3.10+ 的Annotated语法。若你的环境不便使用Annotated,等价写法如下:
commons: CommonQueryParams = Depends(CommonQueryParams)commons = Depends(CommonQueryParams)commons: CommonQueryParams = Depends()官方建议优先使用Annotated版本(因为变量默认值位置用于表达依赖语义更加清晰,且未来兼容性更好)。
总结
- 依赖的本质是callable:函数可以,类可以,任何可调用对象都可以。
- 把类作为依赖时,FastAPI 分析
__init__的参数(self除外),并按路径操作函数参数的规则完成转换、校验与 OpenAPI 文档生成。 - 依赖的解析结果是类的实例,类型注解让 IDE 能提供完整的自动补全与类型检查。
Depends()无参写法是类依赖的快捷语法糖,能消除重复书写类名的冗余。
后续教程将进一步展示如何把类依赖与子依赖、yield依赖、Security等机制组合使用,构建更复杂的依赖注入图。
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考