FastAPI 入门:从路由到依赖注入

本页记录已经学习的 FastAPI 基础:路由、请求参数、请求体与 Pydantic、异步、Depends,以及理解它们所需的 Python 装饰器、闭包与协程概念。目标是建立一条完整主线:请求如何找到函数、数据如何进入函数、函数如何在等待 I/O 时协作,以及请求所需的信息如何被准备好。

1. FastAPI 接口:请求如何到达 Python 函数

from fastapi import FastAPI
 
app = FastAPI()
 
@app.get("/hello")
def hello():
    return {"message": "Hello"}
  • FastAPI() 创建 Web 应用对象;
  • @app.get("/hello") 声明:当收到 GET /hello 时,交给下面的 hello() 函数处理;
  • 函数的返回值会被 FastAPI 转成 HTTP 响应。

可以把路由表理解为:

HTTP Method + Path → Python 函数
 
GET + /hello → hello()

路径必须写在 app.get() 中。只有 @app.get 而没有 "/hello",框架就不知道哪一个 URL 应对应这个函数。


2. Python 装饰器:为什么 @app.get() 能注册路由

2.1 函数也是对象

Python 中函数可以赋给变量、作为参数传入另一个函数,也可以作为返回值返回:

def greet():
    return "hi"
 
f = greet
print(f())  # hi

这里 f 和 greet 都引用同一个函数对象;f() 并不是把函数内容复制了一遍。

2.2 @decorator 的展开形式

@decorator
def hello():
    return "hello"

等价于:

def hello():
    return "hello"
 
hello = decorator(hello)

也就是说,装饰器先接收原函数,再把它的返回结果重新绑定给同名变量 hello。

一个“给函数加日志”的例子:

def log_call(func):
    def wrapper():
        print("调用前")
        result = func()
        print("调用后")
        return result
 
    return wrapper
 
@log_call
def hello():
    return "hello"

现在调用 hello(),实际调用的是 wrapper();但 wrapper() 内部仍通过 func() 调用原来的 hello 函数。

hello 变量 → wrapper 函数 → 原 hello 函数

原函数没有消失,只是不再由 hello 这个名字直接引用。

2.3 闭包:内部函数为什么还能使用外层变量

def outer():
    x = 10
 
    def inner():
        return x
 
    return inner
 
f = outer()
print(f())  # 10

inner() 被返回后,仍持有它所使用的外层变量 x。这种“函数连同其所需外部变量一起保留下来”的现象叫作闭包。

outer() 执行结束,不代表 x 必然被回收:只要 f 还引用着 inner,而 inner 的闭包还引用 x,这条引用链就仍然存在。

f → inner → 闭包中的 x

闭包捕获的是变量绑定,而不是简单复制一个当时的值。因此,如果外层和内部函数共享并修改同一个变量,内部函数可以观察到更新后的值;在内层函数中给该变量重新赋值时,需要使用 nonlocal 表示要修改外层变量。

2.4 FastAPI 的装饰器和普通包装装饰器不同在哪里

普通装饰器常常返回 wrapper,在每次调用函数时加行为。FastAPI 的路由装饰器则主要在应用加载时登记信息:

@app.get("/hello")
def hello():
    return {"message": "Hello"}

可近似理解为:

decorator = app.get("/hello")
hello = decorator(hello)

app.get("/hello") 先得到一个装饰器;该装饰器拿到 hello 后,把“GET /hello 对应 hello”记录到路由表中。它的核心目的不是在每次请求前执行一段包装逻辑。

Python 装饰器与 Spring AOP 都可以表现为“在原逻辑周围增加行为”,但 FastAPI 这里更接近注册路由元数据,不能简单等同于 @Before、@After 或 @Around。


3. 请求参数:URL 与函数参数如何对应

3.1 路径参数(Path Parameter)

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

请求 GET /users/100 时,user_id 得到整数 100。它和 Spring MVC 的 @PathVariable 类似。

/users/100
       ↓
user_id = 100

3.2 类型声明、解析和校验

在普通 Python 中,x: int 主要是类型注解,本身不会自动把字符串转换为整数,也不会强制校验。

FastAPI 会利用这个类型信息:

HTTP 中的 "123"
       ↓
看到 user_id: int
       ↓
解析并校验
       ↓
函数收到 Python int 123

如果请求 /users/abc,它不符合 int,FastAPI 会在进入业务函数前返回参数错误。

3.3 查询参数(Query Parameter)

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

请求:

GET /users/100?detail=true

得到:

user_id == 100
detail is True

判断规则可以先记为:函数参数若出现在路径模板 /xxx/{参数名} 中,就是路径参数;不在路径中且是简单类型时,通常是查询参数。

?page=2&size=10 是 HTTP 的 query string,由 Web 框架解析;它和 Python 的字典解包 func(**data) 是不同概念。

3.4 必填、默认值和可选值

page: int                 # 必填
page: int = 1             # 不传时使用 1
keyword: str | None = None  # 可传字符串或 None;不传时为 None
  • str | None 表示类型允许为 None;
  • = None 表示调用方未提供时的默认值是 None。

二者含义不同。类型是否允许空值,与参数是否有默认值,需要分别判断。


4. 请求体与 Pydantic:JSON 如何成为对象

创建用户时,数据通常放在 HTTP 请求体中:

{
  "name": "Tom",
  "age": 20
}
from fastapi import FastAPI
from pydantic import BaseModel, Field
 
app = FastAPI()
 
class User(BaseModel):
    name: str
    age: int
 
@app.post("/users")
def create_user(user: User):
    return user

BaseModel 不是 Python 原生类,而是 Pydantic 提供的模型基类。FastAPI 会把请求 JSON 交给 Pydantic 解析和校验,再把得到的 User 对象传给函数;这与 Spring 中的 @RequestBody UserDTO 很相似。

HTTP JSON → Python 数据 → Pydantic 解析与校验 → User 对象 → 业务函数

字段约束可以写在模型上:

class User(BaseModel):
    age: int = Field(ge=18)

这类约束负责格式、类型和范围等输入校验,类似 Java Bean Validation。至于“该用户是否已经存在”“库存是否足够”这类业务规则,应放在 Service 或业务代码中,而不是只靠字段校验完成。


5. 异步、协程与 await

5.1 def 和 async def

def hello():
    return "hello"
 
async def async_hello():
    return "hello"

调用普通函数 hello() 会直接执行并得到结果。调用 async_hello() 则先得到一个协程对象(coroutine object),需要由事件循环调度执行。

协程可以理解为“可以在等待时暂停、以后再继续执行的函数”。它尤其适合网络、数据库等 I/O 等待场景。

5.2 await:等待时让出执行机会

await async_operation()

await 用于等待另一个可等待对象完成。若它在等待 I/O,当前协程会暂停并把执行机会交还给事件循环;事件循环可以去推进其他已准备好的协程。

任务 A 执行
  ↓
await I/O,A 暂停
  ↓
事件循环推进任务 B
  ↓
A 的 I/O 完成
  ↓
继续 A

这不是让单个 CPU 同时执行两段 Python 计算,而是避免在 I/O 等待期间空等。

5.3 同步阻塞与异步等待

import asyncio
import time
 
time.sleep(3)          # 当前线程被阻塞 3 秒
await asyncio.sleep(3) # 当前协程等待,事件循环可运行其他任务

关键点是:写成 async def,不代表其中所有代码自动异步。若在 async def 中直接调用 time.sleep()、同步数据库驱动或其他耗时阻塞函数,仍可能堵住事件循环。

5.4 FastAPI 为什么支持两种写法

@app.get("/sync")
def sync_handler():
    ...
 
@app.get("/async")
async def async_handler():
    ...
  • 使用异步 I/O 库时,通常使用 async def 并配合 await;
  • 调用同步阻塞库时,使用普通 def 更合适,FastAPI 可以将同步的路径处理函数放到线程池中执行;
  • 不要为了“看起来异步”而在 async def 内直接运行阻塞式代码。

6. Depends:按一次请求准备所需信息

from fastapi import Depends
 
@app.get("/orders")
def get_orders(user = Depends(get_current_user)):
    return {"user": user}

可以先按下面的顺序理解:

执行 get_orders 前
       ↓
执行并解决 get_current_user
       ↓
把结果作为 user 传入
       ↓
执行 get_orders

Depends 常用于当前用户、Token、数据库 Session 和权限检查等“处理一次请求之前需要先准备”的东西。

6.1 与 Spring DI 的区别

Spring 的依赖注入常表示对象之间的依赖,例如 Controller 依赖 UserService,类似 @Autowired。

FastAPI 的 Depends 更适合从一次请求出发理解:这个处理函数为了完成当前请求,需要什么数据或前置处理? 两者都有“由框架提供依赖”的相似性,但不能简单视为同一个概念。

6.2 登录鉴权中的依赖链

user = Depends(get_current_user)

它可以表示这样的过程:

Request
  ↓
提取 Token
  ↓
验证 Token
  ↓
获得当前用户
  ↓
注入接口函数
  ↓
业务代码

依赖本身也可以继续依赖别的依赖,因此可以形成一条链:

接口 → 权限检查 → 当前用户 → Token → Request

7. 当前掌握范围

FastAPI
├── Python 前置概念:函数对象、装饰器、闭包
├── 路由:@app.get、@app.post、路由表
├── 请求参数:Path、Query、必填/默认/可选
├── 请求体与数据模型:Pydantic、BaseModel、字段校验
├── 异步:async def、协程、await、事件循环、阻塞与异步 I/O
└── 依赖注入:Depends

后续尚未学习的异常处理、响应模型、路由模块化、CRUD、数据库与登录认证不在本页范围内。