← 返回博客
python2026-09-05 21:18:373 分钟 · 991 4

路由、参数与依赖注入:FastAPI 把「参数从哪来」讲透了

FastAPI 的三种参数来源(路径/查询/请求体)如何从类型注解自动解析校验,以及 Depends 依赖注入如何把鉴权、数据库连接、公共逻辑做成可复用的层级结构。

FastAPI 的路由表面看和 Flask 差不多,真正拉开差距的是两件事:参数从声明里自动解析校验,以及 Depends 依赖注入。前者省掉手动取参和校验的代码,后者让鉴权、数据库连接这类横切逻辑不用到处复制粘贴。这篇把这两块讲透。

三种参数来源

FastAPI 按参数声明的类型自动判断它从哪来,不需要你手动 request.args.get()

路径参数

写在 URL 路径里的 {name},函数参数同名接收,类型决定转换和校验:

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return {"user_id": user_id}

user_id: int 会做两件事:把字符串 "123" 转成整数 123;不是整数直接 422。路径里还能用枚举约束取值:

from enum import Enum

class Role(str, Enum):
    admin = "admin"
    user = "user"

@app.get("/roles/{role}")
async def get_role(role: Role):
    return {"role": role.value}

访问 /roles/admin 正常,/roles/root 直接被拒——枚举值成了输入白名单。

查询参数

不在路径里、又有默认值的「简单类型」参数,会被当作查询参数(?key=value):

@app.get("/items/")
async def list_items(skip: int = 0, limit: int = 10, q: str | None = None):
    return {"skip": skip, "limit": limit, "q": q}

必填查询参数就给类型但不给默认值:async def search(q: str)Optional[str] = Nonestr | None = None 等价,后者要 Python 3.10+。

请求体

复杂结构用 Pydantic 模型接收,FastAPI 自动解析 JSON body、校验字段、转成对象:

from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: float
    tags: list[str] = []

@app.post("/items/")
async def create_item(item: Item):
    return {"name": item.name, "price": item.price}

三种来源可以同时出现,FastAPI 按规则各取所需,不会混:

@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, q: str | None = None):
    ...

item_id 来自路径,item 来自 body,q 来自查询字符串。

依赖注入:Depends

这是 FastAPI 最值得花时间理解的设计。语法是一个函数(或可调用对象)用 Depends 声明为参数,框架在收到请求时先调用它、把返回值注入

基础:复用一段逻辑

from fastapi import Depends

def get_query(q: str | None = None):
    return {"q": q}

@app.get("/search/")
async def search(dep: dict = Depends(get_query)):
    return dep

看起来多此一举?当逻辑变复杂就显价值了——比如做公共的参数解析、权限判断、数据预处理。

层级依赖

依赖还能依赖别的依赖,形成调用链:

def get_token(token: str | None = None):
    if not token:
        raise HTTPException(status_code=401, detail="missing token")
    return token

def get_current_user(token: str = Depends(get_token)):
    user = lookup_user(token)  # 伪代码:查库
    if not user:
        raise HTTPException(status_code=401, detail="invalid token")
    return user

@app.get("/me")
async def me(user: dict = Depends(get_current_user)):
    return user

请求进来时执行顺序是 get_token → get_current_user → me。任一层抛 HTTPException,后续不执行,直接返回对应状态码。鉴权逻辑集中在一处,所有需要登录的接口只写一行 Depends(get_current_user)

yield 依赖:资源的获取与释放

需要「用完即清理」的资源(数据库连接、锁、事务)用 yield

from contextlib import asynccontextmanager

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()  # 请求结束一定执行,不论成功或异常

@app.get("/posts/")
async def posts(db: Session = Depends(get_db)):
    return db.query(Post).all()

yield 之前是「请求前」,之后是「请求后」。即使 handler 抛异常,finally 里的 db.close() 也会跑。这和 try/finally 语义一致,只是由框架在正确的时机驱动。

依赖缓存

同一个依赖在同一个请求里被多处引用时,默认只执行一次,结果缓存复用:

async def get_user(user: dict = Depends(get_current_user)): ...
async def update_profile(user: dict = Depends(get_current_user)): ...

同一请求里多处都依赖 get_current_user(比如 handler 本体 + 它的子依赖都声明了它),默认 use_cache=True,整个请求生命周期内只执行一次、结果缓存复用——对「查库取用户」这类操作能省一次往返。只有需要每次都重新执行(如每次都重新校验 token 是否刚过期)时,才用 Depends(get_current_user, use_cache=False) 关闭缓存。

依赖和路由的边界

一个常见误区是把业务逻辑全塞进依赖里。依赖适合放横切关注点:鉴权、DB session、分页参数解析、请求日志。具体业务处理留在 handler 本体。这样每个接口一眼能看懂「它要什么、做什么」。

下一篇讲 Pydantic——上面那些 Itemget_current_user 返回的 user 对象,背后都是用 Pydantic 精确描述「数据长什么样、哪些合法」的。它会让你彻底摆脱手写 if not isinstance(x, int) 的校验代码。

相关推荐

本文为原创文章,采用CC BY-NC-SA 4.0协议授权,转载请保留署名与原文链接。原文链接:https://www.wxbuluo.com/article/169