Python Web 框架:FastAPI 详细使用教程

原文链接:https://xiets.blog.csdn.net/article/details/146187402

版权声明:原创文章禁止转载

专栏目录:Python 专栏(总目录)

文章目录

  • 1. 安装 FastAPI
    • 1.1 Uvicorn 服务器
      • 1.1.1 命令行启动服务
      • 1.1.2 异步处理请求
      • 1.1.3 以编程方式运行
    • 1.2 Hypercorn 服务器
    • 1.3 FastAPI CLI
    • 1.4 可选依赖
    • 1.5 FastAPI
    • 1.6 路径操作函数
  • 2. 请求对象
    • 2.1 路径参数
      • 2.1.1 路径参数简单示例
      • 2.1.2 声明路径参数的类型
      • 2.1.3 路径操作函数匹配顺序
      • 2.1.4 枚举参数值
      • 2.1.5 多路径参数匹配
      • 2.1.6 多个路径参数
      • 2.1.7 路径参数校验
    • 2.2 查询参数
      • 2.2.1 查询参数简单示例
      • 2.2.2 可选参数
      • 2.2.3 查询参数类型转换
      • 2.2.4 枚举查询参数
      • 2.2.5 查询参数校验
      • 2.2.6 数组参数
      • 2.2.7 路径参数与查询参数混用
      • 2.2.8 查询参数模型
    • 2.3 请求头参数
      • 2.3.1 获取请求头参数
      • 2.3.2 请求头参数模型
      • 2.3.3 Cookie 参数
    • 2.4 请求体
      • 2.4.1 请求体数据
      • 2.4.2 JSON 请求体模型
    • 2.5 表单数据
      • 2.5.1 接收表单数据
      • 2.5.2 表单文件上传
    • 2.6 使用请求对象
  • 3. 响应对象
    • 3.1 状态码和响应头(使用响应对象)
    • 3.2 返回响应对象
    • 3.3 JSON响应: JSONResponse
    • 3.4 响应模型: response_model
    • 3.5 HTML响应: HTMLResponse
    • 3.6 重定向响应: RedirectResponse
    • 3.7 流式响应: StreamingResponse
    • 3.8 文件响应: FileResponse
  • 4. 后台任务
  • 5. 静态文件
  • 6. 中间件
    • 6.1 中间件示例
    • 6.2 GZipMiddleware
    • 6.3 TrustedHostMiddleware
    • 6.4 CORSMiddleware
  • 7. 依赖注入
    • 7.1 依赖项函数
    • 7.2 类作为依赖项
    • 7.3 路径操作装饰器依赖项
    • 7.4 子路由依赖项
    • 7.5 全局依赖项
    • 7.6 HTTP Basic Auth
  • 8. 错误处理
    • 8.1 HTTPException
    • 8.2 自定义异常处理器
    • 8.3 覆盖默认的异常处理器
  • 9. 生命周期事件
  • 10. WebSockets 服务
  • 11. APIRouter
  • 12. 子应用挂载

FastAPI 是一个用于构建 API 的现代、快速(高性能)的 Web 框架,使用 Python 并基于标准的 Python 类型提示。

FastAPI 是一个支持 ASGI(Asynchronous Server Gateway Interface)协议的 Web 应用框架,也就是说它同时兼容 ASGI 和 WSGI 的应用。

FastAPI 同时支持同步和异步。FastAPI 天然支持异步协程处理,能快速处理更多的 HTTP 请求。

相关网站:

  • FastAPI GitHub:https://github.com/fastapi/fastapi
  • FastAPI 官网:https://fastapi.tiangolo.com/zh/
  • FastAPI 文档:https://fastapi.tiangolo.com/zh/learn/

1. 安装 FastAPI

安装 FastAPI:

$ pip install fastapi

还需要一个 ASGI 服务器,生产环境 可以使用 Uvicorn 或者 Hypercorn。

生产环境部署 FastAPI 应用程序,参考:https://fastapi.tiangolo.com/zh/deployment/。

1.1 Uvicorn 服务器

Uvicorn 是 Python 的一个高性能、适用于生产环境的 ASGI Web 服务器。Uvicorn 目前支持 HTTP/1.1 和 WebSockets。

以最小的(纯 Python)依赖性安装 uvicorn:

$ pip install "uvicorn[standard]"

完整安装,运行:pip install uvicorn

1.1.1 命令行启动服务

FastAPI 应用示例,创建一个 main.py 源码文件:

from fastapi import FastAPI

app = FastAPI()


@app.get("/ping")
def ping():
    return {"message": "pong"}


@app.get("/items/{item_id}")
def get_item(item_id: str, q: str | None = None):
    return {"item_id": item_id, "q": q}

main.py 文件所在路径,通过命令行启动 uvicorn 服务:

$ uvicorn main:app --reload

INFO:     Will watch for changes in these directories: ['./']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [71991] using WatchFiles
INFO:     Started server process [71993]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

相关命令参数:

  • main: main.py 文件(一个 Python 模块)
  • app:在 main.py 文件中通过 app = FastAPI() 创建的对象。
  • –reload:代码热重载,仅在开发时使用。

服务启动后,访问:

curl "http://127.0.0.1:8000/ping"
curl "http://127.0.0.1:8000/items/123"
curl "http://127.0.0.1:8000/items/123?q=abc"

FastAPI 会对注册的路由自动生成 API 文档,通过链接访问:

  • 交互式 API 文档(由 Swagger UI 生成):http://127.0.0.1:8000/docs
  • 可选的 API 文档(由 ReDoc 生成):http://127.0.0.1:8000/redoc
  • 查看 openapi.json:http://127.0.0.1:8000/openapi.json

如果要禁用以上链接的访问,可以在创建 FastAPI app 实例时传递相应的参数:

app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)

1.1.2 异步处理请求

要想异步处理 HTTP 请求,非常简单,只需要在路由函数前面加上 async 关键字:

from fastapi import FastAPI

app = FastAPI()


@app.get("/ping")
async def ping():
    return {"message": "pong"}


@app.get("/items/{item_id}")
async def get_item(item_id: str, q: str | None = None):
    return {"item_id": item_id, "q": q}

然后以同样的命令启动服务:

$ uvicorn main:app --reload

注意

  • 你可以根据需要在路径操作函数中混合使用 defasync def,并使用最适合你的方式去定义每个函数。FastAPI 将为不同类型的路径操作函数做出正确的处理。
  • 当你使用 def 而不是 async def 来声明一个路径操作函数时,它将运行在外部的线程池中,而不是直接调用(因为它会阻塞线程)。
  • 你也可以直接调用通过 defasync def 创建的任何其他函数,FastAPI 不会影响你调用它们的方式。

1.1.3 以编程方式运行

uvicorn 命令实际上是一个 Python 脚本,最终也是通过 uvicorn 模块提供的方法启动服务。

在代码中就可以直接通过 uvicorn 模块提供的方法以编程方式启动服务:

# 文件: api.py
from fastapi import FastAPI

app = FastAPI()


@app.get("/ping")
async def ping():
    return {"message": "pong"}


@app.get("/items/{item_id}")
async def get_item(item_id: str, q: str | None = None):
    return {"item_id": item_id, "q": q}
# 文件: main.py
import uvicorn

from api import app


def main():
    uvicorn.run(app)
    # 如果要启用 workers 或 reload,则必须将应用程序作为导入字符串传递
    # uvicorn.run("api:app", workers=3)
    # uvicorn.run("api:app", host="127.0.0.1", port=8000, reload=True)


if __name__ == "__main__":
    main()

启动服务:

$ python3 main.py

INFO:     Started server process [72663]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

1.2 Hypercorn 服务器

Hypercorn 是一款基于 sans-io hyper、h11、h2 和 wsproto 库并受 Gunicorn 启发的 ASGI 和 WSGI Web 服务器。

Hypercorn 支持 HTTP/1.1HTTP/2WebSockets(通过 HTTP/1.1 和 HTTP/2)、ASGI 和 WSGI 规范。Hypercorn 可以使用 asyncio、uvloop 或 trio worker 类型。

Hypercorn 可以选择使用 aioquic 库来提供 HTTP/3 规范的当前草案。要启用此功能,请安装 h3 可选的附加组件(pip install hypercorn[h3]),然后选择 quic 绑定,例如 hypercorn --quic-bind localhost:4433

安装 Hypercorn:

pip install hypercorn

FastAPI 应用示例,创建一个 main.py 源码文件:

from fastapi import FastAPI

app = FastAPI()


@app.get("/ping")
async def ping():
    return {"message": "pong"}


@app.get("/items/{item_id}")
async def get_item(item_id: str, q: str | None = None):
    return {"item_id": item_id, "q": q}

通过命令行启动 hypercorn 服务:

$ hypercorn main:app

Hypercorn 也可以通过编程方式使用:

import asyncio

from fastapi import FastAPI
from hypercorn.asyncio import serve
from hypercorn.config import Config

app = FastAPI()


@app.get("/ping")
async def ping():
    return {"message": "pong"}


@app.get("/items/{item_id}")
async def get_item(item_id: str, q: str | None = None):
    return {"item_id": item_id, "q": q}


if __name__ == "__main__":
    asyncio.run(serve(app, Config()))

# 启动服务: python3 main.py

1.3 FastAPI CLI

参考:FastAPI CLI

FastAPI CLI 是一个命令行工具,随着 pip install fastapi 一起安装,可以用它来部署和运行 FastAPI 应用程序,管理 FastAPI 项目。

$ fastapi --help
                                                                                
 Usage: fastapi [OPTIONS] COMMAND [ARGS]...                                     
                                                                                
 FastAPI CLI - The fastapi command line app.                                  
 Manage your FastAPI projects, run your FastAPI apps, and more.                 
                                                                                
 Read more in the docs: https://fastapi.tiangolo.com/fastapi-cli/.              
                                                                                
╭─ Options ────────────────────────────────────────────────────────────────────╮
│ --version                     Show the version and exit.                     │
│ --install-completion          Install completion for the current shell.      │
│ --show-completion             Show completion for the current shell, to copy │
│                               it or customize the installation.              │
│ --help                        Show this message and exit.                    │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────────╮
│ dev   Run a FastAPI app in development mode.                               │
│ run   Run a FastAPI app in production mode.                                │
╰──────────────────────────────────────────────────────────────────────────────╯

主要包含两个子命令:

  • fastapi dev:以开发模式运行程序,默认启用热重载。
  • fastapi run:以生产环境模式运行程序,默认禁用热重载。

这两个子命令会根据传入的文件或目录路径自动检测需要导入的 Python 模块或包。如果没有传递路径,它会尝试使用:

  • main.py
  • app.py
  • api.py
  • app/main.py
  • app/app.py
  • app/api.py

并在默认情况下,它会在模块或包中查找名为 appapi 的 FastAPI 实例变量。

fastapi dev|run 内部使用 Uvicorn 作为 ASGI 服务器。因此需要确保 Uvicorn 已安装。

FastAPI CLI 使用示例:

# main.py

from fastapi import FastAPI

app = FastAPI()


@app.get("/ping")
async def ping():
    return {"message": "pong"}


@app.get("/items/{item_id}")
async def get_item(item_id: str, q: str | None = None):
    return {"item_id": item_id, "q": q}

运行 FastAPI 应用程序:

$ fastapi dev               # 在当前目录下查找要运行的模块(main.py),模块文件内需要有一个 FastAPI 实例(app 或 api)

# 或者指定要运行的模块文件

$ fastapi dev main.py       # 手动指定运行的模块文件,文件内需要有一个 FastAPI 实例(app 或 api)

1.4 可选依赖

FastAPI 内部使用 Starlette 处理 Web,使用 Pydantic 处理数据。

FastAPI 可选的依赖库:

  • 用于 Pydantic:
    • email-validator:用于 email 校验。
  • 用于 Starlette:
    • httpx:使用 TestClient 时安装。
    • jinja2:使用默认模板配置时安装。
    • python-multipart:需要通过 request.form() 对表单进行「解析」时安装。
    • itsdangerous:需要 SessionMiddleware 支持时安装。
    • pyyaml:使用 Starlette 提供的 SchemaGenerator 时安装(有 FastAPI 你可能并不需要它)。
    • graphene:需要 GraphQLApp 支持时安装。
  • 用于 FastAPI / Starlette:
    • uvicorn:用于加载和运行你的应用程序的服务器。
    • orjson:使用 ORJSONResponse 时安装。
    • ujson:使用 UJSONResponse 时安装。

你也可以通过 pip install "fastapi[all]" 命令安装上面的所有依赖。

1.5 FastAPI

FastAPI 类表示一个应用实例。

导入 FastAPI,创建应用实例:

from fastapi import FastAPI

app = FastAPI(...)

FastAPI 构造方法常用参数:

  • debugbool,是否开启调试模式。开启调试模式后,在服务器出错时返回错误调用栈。
  • titlestr,API 的标题。它将被添加到生成的 OpenAPI 中。
  • summarystr,API 的简短摘要。它将被添加到生成的 OpenAPI 中。
  • descriptionstr,API 的描述,支持 Markdown。它将被添加到生成的 OpenAPI 中。
  • versionstr,API 的版本。应用程序的版本(不是 OpenAPI 规范的版本)。它将被添加到生成的 OpenAPI 中。
  • openapi_urlOptional[str],默认值 "/openapi.json",提供 OpenAPI 模式的 URL。None 表示不会公开提供任何 OpenAPI 模式。
  • docs_urlOptional[str],默认值 "/docs",自动交互式 API 文档的路径。None 表示禁用,如果 openapi_url=None,它将被自动禁用。
  • redoc_urlOptional[str],默认值 "/redoc",替代自动交互式 API 文档的路径。None 表示禁用,如果 openapi_url=None,它将被自动禁用。
  • root_pathstr,路径前缀。

1.6 路径操作函数

在 FastAPI 中,「路径操作函数」(Path Operation Function)是指与特定 HTTP 方法和路径(URL)相关联的函数。当客户端请求与该路径和方法匹配时,FastAPI 会调用这个函数来处理请求并返回响应。

路径操作函数通常与 HTTP 方法(如 GET、POST、PUT、DELETE 等)相关联。FastAPI 提供了 @app.get()@app.post() 等装饰器来定义路径操作函数。

下面代码中定义了一个「路径操作函数」:

  • 路径是 /
  • 操作是 GET
  • 函数是 root()
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

你可以返回一个 dictliststrint 等等,FastAPI 会自动对返回值转换为对应的内容类型返回给客户端。

通过路径操作函数的参数接收路径参数、查询参数、请求头、请求体等信息,FastAPI 会自动解析这些数据,并根据参数的类型自动传递给对应的参数。

2. 请求对象

FastAPI 通过路径操作函数中的参数接收请求参数,支持包括路径参数、查询参数、请求头参数、请求体参数 等多种类型的请求参数。

FastAPI 可以根据路径操作函数参数的名称和类型自动接收和转换对应名称的路径参数或查询参数。路径操作函数的参数也可以赋值一个请求参数类型默认值(如:Path()Query()Header() 等),显式地声明为路径参数、查询参数、请求头参数等。

用于显式地声明为请求参数类型的函数类型包括:

  • fastapi.Path():用于声明路径参数。
  • fastapi.Query:用于声明查询参数。
  • fastapi.Header:用于声明请求头参数。
  • fastapi.Body:用于声明请求体参数。
  • fastapi.Form:用于声明表单参数。
  • fastapi.Cookie:用于声明 Cookie 参数。
  • fastapi.File:用于声明文件参数。

除了使用路径操作函数的普通类型参数接收请求参数外,还可以使用请求参数模型类(继承自 pydantic.BaseModel 的类)接收多个请求体参数。

2.1 路径参数

FastAPI 支持使用 Python 字符串格式化语法声明路径参数(变量)。

2.1.1 路径参数简单示例

路径参数简单示例:

from fastapi import FastAPI

app = FastAPI()

@app.get("/items/{item_id}")
async def root(item_id: str):
    return {"item_id": item_id}

路径参数的 {item_id} 值将自动传递给传递给函数的 item_id 参数。访问路径:

$ curl -i http://127.0.0.1:8000/items/abc123
HTTP/1.1 200 OK
server: uvicorn
content-length: 20
content-type: application/json

{"item_id":"abc123"}

2.1.2 声明路径参数的类型

使用 Python 标准类型注解,声明路径操作函数中路径参数的类型,FastAPI 会自动把参数值转换为对应的类型,如果类型错误,将返回错误。

下面代码中,指定路径参数 item_id 的类型为 int

from fastapi import FastAPI

app = FastAPI()

@app.get("/items/{item_id}")
async def root(item_id: int):
    return {"item_id": item_id}

访问路径,正确传递参数类型:

$ curl -i http://127.0.0.1:8000/items/123   
HTTP/1.1 200 OK
content-type: application/json

{"item_id":123}

访问路径,错误传递参数类型:

$ curl -i http://127.0.0.1:8000/items/abc
HTTP/1.1 422 Unprocessable Content
content-type: application/json

{
  "detail": [
    {
      "type": "int_parsing",
      "loc": [
        "path",
        "item_id"
      ],
      "msg": "Input should be a valid integer, unable to parse string as an integer",
      "input": "abc"
    }
  ]
}

2.1.3 路径操作函数匹配顺序

不包含路径参数和包含路径参数操作的函数匹配与函数书写顺序有关。例如 /users/self/users/{user_id},当输入前者时,后者的路径也可以匹配。

路径操作函数的匹配是按加载顺序匹配的,因此 /users/self 需要写在 /users/{user_id} 的前面:

from fastapi import FastAPI

app = FastAPI()

@app.get("/users/self")
async def users_self():
    return {"user": "self"}

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

2.1.4 枚举参数值

如果路径参数的值是固定的若干个值之一,可以使用 Python 的枚举类型接收。FastAPI 会把路径参数值自动转换为对应的枚举值(根据枚举元素的 value 值匹配,包括值类型),如果没有匹配的枚举值,将会返回错误。

枚举值参数代码示例:

import enum
from fastapi import FastAPI

app = FastAPI()

@enum.unique
class Color(str, enum.Enum):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"

@app.get("/colors/{color}")
async def colors(color: Color):
    # 枚举变量可以直接输出 (输出为value)
    return {"color": color, "color_name": color.name, "color_value": color.value}

访问路径,使用正确的参数值:

$ curl -i http://127.0.0.1:8000/colors/red
HTTP/1.1 200 OK
content-type: application/json

{
  "color": "red",
  "color_name": "RED",
  "color_value": "red"
}

访问路径,使用错误的参数值:

$ curl -i http://127.0.0.1:8000/colors/yellow 
HTTP/1.1 422 Unprocessable Content
content-type: application/json

{
  "detail": [
    {
      "type": "enum",
      "loc": [
        "path",
        "color"
      ],
      "msg": "Input should be 'red', 'green' or 'blue'",
      "input": "yellow",
      "ctx": {
        "expected": "'red', 'green' or 'blue'"
      }
    }
  ]
}

2.1.5 多路径参数匹配

操作路径中的参数默认只匹配单个路径,可以在参数后面加上 :path 表示匹配一个路径,而不是简单的一个参数:

from fastapi import FastAPI

app = FastAPI()

@app.get("/files/{file_path:path}")
async def read_file(file_path: str):
    return {"file_path": file_path}

{file_path:path} 表示 file_path 参数匹配的是一个路径(不包括默认的查询参数):

$ curl http://127.0.0.1:8000/files/aa/bb/ 
{
  "file_path": "aa/bb/"
}

$ curl http://127.0.0.1:8000/files/aa/bb/cc.txt
{
  "file_path": "aa/bb/cc.txt"
}

2.1.6 多个路径参数

一个路径中,可以包括多个路径参数:

from fastapi import FastAPI

app = FastAPI()

@app.get("/groups/{group_id}/files/{file_id}")
async def read_file(group_id: int, file_id: int):
    return {"group_id": group_id, "file_id": file_id}

访问路径:

$ curl http://127.0.0.1:8000/groups/123/files/456
{
  "group_id": 123,
  "file_id": 456
}

2.1.7 路径参数校验

FastAPI 提供了 fastapi.Path 函数类型,用于对路径参数进行校验。Path() 类型的参数可以为路径参数指定最大值、最小值、正则表达式等,还可以为路径参数添加元数据。

路径操作函数中使用 Path() 函数调用给路径参数初始化,则表示该参数显式声明为路径参数,并且受到 Path() 函数中参数的约束。

from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/groups/{group_id}/items/{item_id}")
async def read_items(group_id: int = Path(gt=0), item_id: str = Path(regex="^[a-z]+$")):
    """
    - group_id: int 类型,参数值必须大于0。
    - item_id: str 类型,必须是小写字母。
    """
    return {"group_id": group_id, "item_id": item_id}

2.2 查询参数

路径操作函数声明的参数不是路径参数时,会把该参数自动解释为查询参数(即优先解析为路径参数)。所有应用于路径参数的机制也适用于查询参数,例如:数据解析、数据校验、枚举参数等。

2.2.1 查询参数简单示例

带查询参数的路径操作函数:

from fastapi import FastAPI

app = FastAPI()

@app.get("/search")
async def search(keyword: str, count: int):
    return {"keyword": keyword, "count": count}

带查询参数访问路径:

$ curl "http://127.0.0.1:8000/search?keyword=hello&count=10" 
{
  "keyword": "hello",
  "count": 10
}

2.2.2 可选参数

查询参数默认是必选参数,如果设置默认值,则为可选参数:

from fastapi import FastAPI

app = FastAPI()

@app.get("/search")
async def search(keyword: str, count: int = 10):
    return {"keyword": keyword, "count": count}

访问路径:

$ curl "http://127.0.0.1:8000/search?keyword=hello"         
{
  "keyword": "hello",
  "count": 10
}

2.2.3 查询参数类型转换

查询参数可以声明为 bool 类型,FastAPI 会自动把参数值转换为布尔值:

from fastapi import FastAPI

app = FastAPI()

@app.get("/search")
async def search(search_all: bool = False):
    return {"search_all": search_all}

访问路径:

$ curl "http://127.0.0.1:8000/search?search_all=1" 
{"search_all":true}

$ curl "http://127.0.0.1:8000/search?search_all=0"
{"search_all":false}

$ curl "http://127.0.0.1:8000/search?search_all=true"
{"search_all":true}

curl "http://127.0.0.1:8000/search?search_all=false"
{"search_all":false}

还可以使用其他支持解析为 bool 类型的字符串,如 yesno

2.2.4 枚举查询参数

枚举查询参数代码示例:

import enum
from fastapi import FastAPI

app = FastAPI()

@enum.unique
class Color(str, enum.Enum):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"

@app.get("/colors/")
async def colors(color: Color):
    return {"color": color, "color_name": color.name, "color_value": color.value}

访问路径:

$ curl "http://127.0.0.1:8000/colors/?color=red" 
{
  "color": "red",
  "color_name": "RED",
  "color_value": "red"
}

2.2.5 查询参数校验

使用 fastapi.Query 类型给查询参数赋初始值,则该参数将被显式地声明为查询参数,可以对查询参数进行校验,例如:设置初始值、最大值、最小值、正则表达式、参数元数据等。

from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/search")
async def search(keyword: str = Query(min_length=1, max_length=50), count: int = Query(10, gt=0, le=30)):
    """
    - keyword: str 类型,长度在 1 到 50 之间。没有提供默认值,则为必选。
    - count: int 类型,大于 0 且小于等于 30,默认值为 10。
    """
    return {"keyword": keyword, "count": count}

2.2.6 数组参数

将函数的参数赋值为一个 fastapi.Query 类型的默认值显式地将参数声明为查询参数,则该查询参数支持以数组的方式接收多个参数值,数组参数的类型为列表类型。

import enum
from fastapi import FastAPI, Query

app = FastAPI()

@enum.unique
class Color(str, enum.Enum):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"

@app.get("/colors/")
async def colors(color: list[Color] = Query(...)):
    """
    其中 `Query(...)` 中的参数为 `...` 表示该参数必须至少提供一个,否则返回错误。
    """
    return {"color": color}

访问路径:

$ curl "http://127.0.0.1:8000/colors/?color=red&color=green"
{
  "color": ["red", "green"]
}

如果数组参数为可选参数,可以在 Query() 中给一个默认值:

import enum
from fastapi import FastAPI, Query

app = FastAPI()

@enum.unique
class Color(str, enum.Enum):
    RED = "red"
    GREEN = "green"
    BLUE = "blue"

@app.get("/colors/")
async def colors(color: list[Color] = Query(None)):
    """
    color 查询参数的默认值为 None
    """
    return {"color": color}

2.2.7 路径参数与查询参数混用

路径参数与查询参数混合使用示例:

from fastapi import FastAPI

app = FastAPI()

@app.get("/groups/{group_id}/search")
async def search(group_id: int, keyword: str):
    return {"group_id": group_id, "keyword": keyword}

访问路径:

$ curl "http://127.0.0.1:8000/groups/123/search?keyword=hello"             
{
  "group_id": 123,
  "keyword": "hello"
}

2.2.8 查询参数模型

如果查询参数过多,可以使用 Pydantic 模型类来组织查询参数,同时可以利用 Pydantic 的数据校验功能。

from fastapi import FastAPI, Query
from pydantic import BaseModel, Field

app = FastAPI()

class SearchParams(BaseModel):
    keyword: str = Field(min_length=1, max_length=50)   # str 类型,长度在 1 到 50 之间。没有提供默认值,则为必选。
    count: int = Field(10, gt=0, le=30)                 # int 类型,大于 0 且小于等于 30,默认值为 10。

@app.get("/search")
async def search(search_params: SearchParams = Query()):
    return {"keyword": search_params.keyword, "count": search_params.count}

访问路径:

$ curl "http://127.0.0.1:8000/search?keyword=hello&count=20"
{
  "keyword": "hello",
  "count": 20
}

2.3 请求头参数

定义请求头参数的方式与 PathQuery 参数相同,可以使用 fastapi.Header 类型。

2.3.1 获取请求头参数

使用 Header() 把参数显式地声明为请求头参数,获取请求头参数示例:

from fastapi import FastAPI, Header

app = FastAPI()

@app.get("/items")
async def read_items(host: str = Header(), user_agent: str = Header(), x_token: list[str] = Header(None)):
    # x_token 为 list[str] 类型,表示以数组的方式接收多个值。
    return {"host": host, "user_agent": user_agent, "x_token": x_token}

大部分标准请求头通常使用连接符(中划线)分隔,如:User-AgentHeader 默认情况下会把下划线自动转换为中划线后再匹配参数,并且不区分大小写。例如:user_agent 会匹配 User-Agent

如果要禁用下划线自动转换为连接符,可以把 Header()convert_underscores 参数设置为 False

2.3.2 请求头参数模型

使用 Pydantic 模型类接收一组请求头参数:

from fastapi import FastAPI, Header
from pydantic import BaseModel, Field

app = FastAPI()

class CommonHeaders(BaseModel):
    host: str = Field("default value")
    user_agent: str = Field("default value")

@app.get("/items")
async def read_items(headers: CommonHeaders = Header()):
    return {"host": headers.host, "user_agent": headers.user_agent}

2.3.3 Cookie 参数

Cookie 参数是携带在请求头中的,可以使用 Header() 参数类型接收 Cookie 请求头的值,但不会解析 Cookie。

使用 fastapi.Cookie 函数类型显式地声明 Cookie 参数,自动接收并解析对应名称的 Cookie 值。

from fastapi import FastAPI, Cookie

app = FastAPI()

@app.get("/items")
async def read_items(sid: str = Cookie(None), token: str = Cookie(None)):
    return {"sid": sid, "token": token}

访问路径:

$ curl -H "Cookie: sid=hello; token=world" http://127.0.0.1:8000/items
{
  "sid": "hello",
  "token":"world"
}

Cookie 参数也可以通过 pydantic.BaseModel 模型来接收:

from fastapi import FastAPI, Cookie
from pydantic import BaseModel, Field

app = FastAPI()

class Cookies(BaseModel):
    sid: str = Field("default value")
    token: str = Field("default value")

@app.get("/items")
async def read_items(cookies: Cookies = Cookie(None)):
    return {"sid": cookies.sid, "token": cookies.token}

2.4 请求体

接收请求体(Body)的数据,使用 fastapi.Body() 类型显式声明参数。

2.4.1 请求体数据

有请求体的请求方式通常是 POST、PUT 等,可以使用 bytesstr 类型接收通用的请求体数据:

from fastapi import FastAPI, Body

app = FastAPI()

@app.post("/items")
async def post_items(body: bytes = Body(b"")):
    return {"body": body.decode(encoding="utf-8")}

访问路径:

$ curl -X POST -d "hello" http://127.0.0.1:8000/items
{
  "body": "hello"
}

2.4.2 JSON 请求体模型

如果请求体是 JSON 格式的数据,可以使用 Pydantic 模型类接收请求体数据:

from fastapi import FastAPI, Body
from pydantic import BaseModel, Field

app = FastAPI()

class Item(BaseModel):
    name: str = Field("")
    age: int = Field(0)
    salary: float = Field(0.0)
    is_male: bool = Field(False)

@app.post("/items")
async def post_items(item: Item | None = Body(None)):
    print("item json:", item.model_dump_json())
    return {"name": item.name, "age": item.age, "salary": item.salary, "is_male": item.is_male}

访问路径:

$ curl -X POST \
  -d '{"name": "Tom", "age": 18, "salary": 99999.999, "is_male": true}' \
  -H "Content-Type: application/json" \
  http://127.0.0.1:8000/items

{
    "name": "Tom",
    "age": 18,
    "salary": 99999.999,
    "is_male": true
}

Pydantic 模型也支持数组和嵌套对象:

from typing import Optional
from fastapi import FastAPI, Body
from pydantic import BaseModel, Field

app = FastAPI()

class Item(BaseModel):
    name: str = Field("")
    age: int = Field(0)
    salary: float = Field(0.0)
    is_male: bool = Field(False)
    tags: list[str] | None = Field(None)
    sub_item: Optional["SubItem"] = Field(None)

class SubItem(BaseModel):
    name: str = Field("")
    desc: str = Field("")

@app.post("/items")
async def post_items(item: Item | None = Body(None)):
    return {"item_json": item.model_dump_json()}

2.5 表单数据

表单数据使用 fastapi.Form() 类型显式声明参数。

要使用表单,需要先安装 python-multipart 库:

pip install python-multipart

不包含文件字段的简单表单通常以 application/x-www-form-urlencoded 格式编码,包含文件字段的表单以 multipart/form-data 格式编码。

2.5.1 接收表单数据

From() 声明的参数同时支持 application/x-www-form-urlencodedmultipart/form-data 编码的表单数据:

接收表单数据示例:

from fastapi import FastAPI, Form

app = FastAPI()

@app.post("/login")
async def login(username: str = Form(), password: str = Form()):
    return {"username": username, "password": password}

使用 application/x-www-form-urlencoded 编码提交表单:

$ curl -v --data-urlencode "username=hello" --data-urlencode "password=world" http://127.0.0.1:8000/login
> Host: 127.0.0.1:8000
> Content-Type: application/x-www-form-urlencoded
> 
< HTTP/1.1 200 OK
< content-type: application/json
< 
{"username":"hello","password":"world"}

使用 multipart/form-data 编码提交表单:

$ curl -v -F "username=hello" -F "password=world" http://127.0.0.1:8000/login
> Host: 127.0.0.1:8000
> Content-Type: multipart/form-data; boundary=******
> 
< HTTP/1.1 200 OK
< content-type: application/json
< 
{"username":"hello","password":"world"}

使用 Pydantic 模型类接收表单数据:

from fastapi import FastAPI, Form
from pydantic import BaseModel, Field

app = FastAPI()

class MyFormData(BaseModel):
    username: str = Field()
    password: str = Field()

@app.post("/login")
async def login(form_data: MyFormData = Form()):
    return {"username": form_data.username, "password": form_data.password}

2.5.2 表单文件上传

表单中的文件字段可以使用 fastapi.File() 类型显式声明参数,接收表单文件字段的参数类型为 UploadFile。

接收包含普通字段和文件字段表单的数据:

from fastapi import FastAPI, Form, File, UploadFile

app = FastAPI()

@app.post("/create")
async def create(text: str = Form(), file: UploadFile = File()):
    # 异步读取文件字段内容
    file_content = await file.read()
    await file.close()
    return {
        "text": text,
        "filename": file.filename,
        "file_size": file.size,
        "file_content_type": file.content_type,
        "file_headers": file.headers,
        "file": file_content.decode(encoding="utf-8")
    }

提交包含文件字段的表单:

$ curl -F "text=hello" -F "[email protected]" http://127.0.0.1:8000/create
{
  "text": "hello",
  "filename": "world.txt",
  "file_size": 14,
  "file_content_type": "text/plain",
  "file_headers": {
    "content-disposition": "form-data; name=\"file\"; filename=\"world.txt\"",
    "content-type": "text/plain"
  },
  "file": "你好\n世界\n"
}

如果要提交多个字段名称相同的表单文件,可以使用 list[UploadFile] 类型接收:

from fastapi import FastAPI, File, UploadFile

app = FastAPI()

@app.post("/create")
async def create(files: list[UploadFile] = File(None, alias="file")):
    for file in files:
        content = await file.read()
        await file.close()
        print(file.headers)
        print(content.decode(encoding="utf-8"))
    return { }

2.6 使用请求对象

FastAPI 使用 Request 类型表示请求对象,包含了请求的方法、路径、查询参数、请求头、请求体等。

可以在路径操作函数中使用 Request 类型参数接收请求对象,FastAPI 会把请求对象传递给对应的参数。

from fastapi import FastAPI, Request

app = FastAPI()

@app.get("/items")
async def read_items(request: Request):
    print(f"client: {request.client.host}:{request.client.port}")
    print(f"method: {request.method}")
    print(f"url: {request.url}")
    print(f"query_params: {request.query_params}")
    print(f"headers: {request.headers}")
    print(f"body: {await request.body()}")
    return {"hello": "world"}

3. 响应对象

3.1 状态码和响应头(使用响应对象)

FastAPI 默认返回状态码为 200,可以在装饰器函数中使用 status_code 参数修改返回的默认状态码。

from fastapi import FastAPI, status

app = FastAPI()

@app.post("/create", status_code=status.HTTP_201_CREATED)   # 默认状态码改为 201
async def create():
    return { }

如果需要在路径操作函数中动态返回错误码,或者需要添加响应头,可以在函数参数中增加 Response 类型参数,FastAPI 将会把响应对象传进来。

from fastapi import FastAPI, status
from fastapi.responses import Response

app = FastAPI()

@app.post("/create")
async def create(res: Response):
    res.status_code = status.HTTP_201_CREATED   # 设置本次响应的状态码
    res.headers["X-Custom-Header"] = "custom"   # 设置本次响应的响应头
    # res.set_cookie(...)                       # 设置本次响应的Cookie
    return { }

3.2 返回响应对象

FastAPI 使用 Response 表示响应对象,包含了响应状态码、响应头、响应内容等。

可以直接使用 Response 响应对象,也可以使用它的子类:JSONResponse、HTMLResponse、PlainTextResponse、RedirectResponse、StreamingResponse、FileResponse 等。

手动构造 Response 对象返回:

import json
from fastapi import FastAPI, Response, status

app = FastAPI()

@app.get("/items")
async def items() -> Response:
    content = json.dumps({ "hello": "你好", "world": "世界" }, ensure_ascii=False)
    return Response(
        content=content.encode(encoding="utf-8"),
        status_code=status.HTTP_200_OK,
        headers={ "X-Custom-Header": "custom" },    # 自动包含 Content-Length,它还将包含一个基于 media_type 的 Content-Type。
        media_type="application/json; charset=utf-8"
    )

3.3 JSON响应: JSONResponse

FastAPI 默认使用 JSONResponse 响应对象返回 JSON 格式的数据。

当你在路径操作函数中返回一个 distliststr 等数据时,FastAPI 会自动把数据转换为 JSON 格式返回给客户端,并自动设置 Content-Type: application/json

from fastapi import FastAPI

app = FastAPI()

@app.get("/items")
async def items():
    return { "hello": "你好", "world": "世界" }

使用 FastAPI 提供了 jsonable_encoder 编码器可以将任何对象转换为可以用 JSON 编码的内容(可以被 json.dumps() 编码的对象),然后手动构造 JSONResponse 对象返回:

from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse

app = FastAPI()

@app.get("/items")
async def items() -> JSONResponse:
    content = { "hello": "你好", "world": "世界" }
    json_obj = jsonable_encoder(content)
    return JSONResponse(json_obj)

3.4 响应模型: response_model

可以在任意路径操作注解中使用 response_model 参数来声明用于响应的模型,并配合以下参数可以控制要如何输出模型中的字段:

  • response_model_exclude_unset:不输出默认值字段,TrueFalse
  • response_model_include:要包括的字段,strset[str] 类型。
  • response_model_exclude:不包括的字段,strset[str] 类型。
from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class ResData(BaseModel):
    """
    响应模型
    """
    name: str = Field("")
    age: int = Field(0)
    desc: str | None = Field(None)

@app.get("/items", response_model=ResData, response_model_exclude_unset=True)
async def items() -> ResData:
    res_data = ResData(name="Tom", age=18)
    # FastAPI 默认将模型对象使用 JSONResponse 返回
    return res_data

访问路径:

$ curl -i http://127.0.0.1:8000/items
HTTP/1.1 200 OK
content-length: 23
content-type: application/json

{"name":"Tom","age":18}

3.5 HTML响应: HTMLResponse

返回 HTML 响应:

from fastapi import FastAPI, status
from starlette.responses import HTMLResponse

app = FastAPI()

@app.get("/items")
async def items():
    html = """
    
        
            HTML标题
        
        
            

HTML内容

"""
return HTMLResponse(content=html, status_code=status.HTTP_200_OK)

FastAPI 可以使用 Jinja2 模版引擎渲染 HTML 模板,参考:FastAPI 模板。

3.6 重定向响应: RedirectResponse

返回 RedirectResponse 重定向响应:

from fastapi import FastAPI
from starlette.responses import RedirectResponse

app = FastAPI()

@app.get("/items")
async def items():
    return RedirectResponse("https://typer.tiangolo.com")

3.7 流式响应: StreamingResponse

如果需要返回大块数据,可以使用 StreamingResponse 流式返回响应。StreamingResponse 可以接收一个同步或异步的流对象。

from fastapi import FastAPI
from starlette.responses import StreamingResponse
import io

app = FastAPI()

def generate_numbers():
    """
    返回一个生成器
    """
    for i in range(1, 11):
        yield f"{i}\n"

@app.get("/items")
async def read_items():
    generator = generate_numbers()
    # 给流响应传递一个生成器,它将通过迭代生成器逐块获取数据并返回。
    return StreamingResponse(generator, media_type="text/plain")


@app.get("/content")
async def read_content():
    buf = io.StringIO("Hello\n" * 10000)
    # 传递一个流对象
    return StreamingResponse(buf, media_type="text/plain")

使用 StreamingResponse 返回文件数据:

from fastapi import FastAPI
from starlette.responses import StreamingResponse

app = FastAPI()

@app.get("/file")
def read_file():
    file_path = "path/to/your/file"
    file_like = open(file_path, mode="rb")
    # 文件对象就是一个流对象
    return StreamingResponse(file_like, media_type="application/octet-stream")

上面的路径操作函数内使用了同步读取文件的 open() 函数,因此路径操作函数也使用同步的。如果需要异步读取文件,可以使用 aiofiles。

3.8 文件响应: FileResponse

如果要返回一个文件数据,可以直接使用 FileResponse 作为响应对象,常用的参数有:

  • path:要流式传输的文件的文件路径。
  • headers:自定义请求体,字典类型。
  • media_type:文件的 MIME 类型。如果未设置,则根据文件名或路径推断 MIME 类型。
  • filename:下载文件名。如果给出,它将包含在 Content-Disposition 响应头中。
from fastapi import FastAPI
from starlette.responses import FileResponse

app = FastAPI()

@app.get("/file")
async def read_file():
    file_path = "path/to/your/file"
    return FileResponse(file_path)

FileResponse 响应将包括适当的 Content-LengthLast-ModifiedETagAccept-Ranges 响应头。但 FileResponse 并不支持 Last-ModifiedETagRange 请求头,要想正确处理这些请求头,需要手动实现这些功能。

4. 后台任务

有些请求需要在请求之后执行一些耗时操作,但客户端不必等待这些操作完成,可以使用后台任务来处理这些操作。例如:发送邮件、写入日志、清理缓存等。

FastAPI 提供了 BackgroundTasks 类来处理后台任务,后台任务是异步执行的函数,并且不会阻塞主请求。

请求中使用后台任务, 需要在路径操作函数中声明一个 BackgroundTasks 类型的参数,然后使用该参数对象的 add_task() 方法添加后台任务。

import time
from fastapi import FastAPI, BackgroundTasks

app = FastAPI()

def send_email(email: str, message: str = ""):
    """
    后台任务函数,可以是 `async def` 或 `def` 函数,FastAPI 知道如何正确处理。
    """
    # 模拟发送邮件耗时操作
    time.sleep(5)
    print(f"Email to {email} sent with message: {message}")

@app.post("/send-notification/{email}")
async def send_notification(email: str, background_tasks: BackgroundTasks):
    # 添加后台任务,第一个参数是函数名,后面的参数是函数的参数
    background_tasks.add_task(send_email, email, message="Hello")
    return {"message": "Notification scheduled"}

5. 静态文件

FastAPI 可以使用 StaticFiles 类来提供 静态文件服务,例如:图片、CSS、JavaScript 等。

StaticFiles 类的构造函数:

StaticFiles(
    *,
    directory=None,
    packages=None,
    html=False,
    check_dir=True,
    follow_symlink=False
)

使用 FastAPI 的 mount() 方法挂载静态文件服务:

from fastapi import FastAPI
from starlette.staticfiles import StaticFiles

app = FastAPI()

# 访问 GET "/static" 路径时将访问本地的 "./static" 目录
app.mount("/static", StaticFiles(directory="static"), name="static")

StaticFiles 自动支持 Etag/If-None-MatchLast-Modified/If-Modified-SinceAccept-Ranges/Range 等响应/请求头。

6. 中间件

中间件是一个函数,它接收一个请求和一个处理请求的函数,然后返回一个响应。中间件可以在请求处理之前和之后执行额外的操作,例如:记录日志、修改请求、修改响应等。

6.1 中间件示例

FastAPI 使用 @app.middleware("http") 装饰器来添加中间件(目前只支持http中间件),中间件函数的参数是一个请求对象和一个处理请求的函数,返回一个响应对象。

下面代码示例中使用中间件计算请求处理时间:

import time
from fastapi import FastAPI, Request, Response

app = FastAPI()

@app.middleware("http")
async def add_process_time_header(request: Request, call_next: callable) -> Response:
    # 请求处理前
    start_time = time.perf_counter()

    # 调用下一个中间件或路径操作函数
    response = await call_next(request)

    # 请求处理后
    process_time = time.perf_counter() - start_time
    response.headers["X-Process-Time"] = f"{process_time:.6f} sec"
    return response

@app.get("/items")
async def read_items():
    return { "hello": "world" }

除了使用 @app.middleware("http") 装饰器添加中间件外,还可以使用 app.middleware() 方法添加中间件(继承自 BaseHTTPMiddleware 的中间件类):

from fastapi import FastAPI, Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import ASGIApp

app = FastAPI()

class ChangePathMiddleware(BaseHTTPMiddleware):
    """
    自定义中间件
    """

    def __init__(self, app: ASGIApp, path: str) -> None:
        super().__init__(app)
        self.path = path

    async def dispatch(self, request: Request, call_next) -> Response:
        # 修改请求路径
        if request.scope["path"] == self.path:
            request.scope["path"] = "/new-path"
        response = await call_next(request)
        return response


# 添加自定义中间件。第一个参数是中间件类名,后面参数是传递给中间件类的构造函数的参数(`app`参数不用传)。
app.add_middleware(ChangePathMiddleware, path="/items")

@app.get("/new-path")
async def new_path():
    return { "message": "Path changed" }

@app.get("/items")
async def read_items():
    return { "hello": "world" }

除了自己定义中间件外,FastAPI 还提供了一些内置的中间件,例如:GZipMiddleware、TrustedHostMiddleware 、CORSMiddleware、HTTPSRedirectMiddleware 等。

6.2 GZipMiddleware

GZipMiddleware 中间件用于 gzip 压缩响应数据,可以减少传输数据量,提高传输速度。

from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware

app = FastAPI()

# 添加 gzip 压缩中间件。第一个参数是中间件类名,后面参数是传递给中间件类的构造函数的参数。
app.add_middleware(GZipMiddleware, minimum_size=500, compresslevel=9)

@app.get("/items")
async def read_items():
    return { "hello": "world" * 1000}

6.3 TrustedHostMiddleware

TrustedHostMiddleware 中间件用于检查请求的 Host 头是否在信任的主机列表中,如果不在列表中则返回 400 错误。

from fastapi import FastAPI
from fastapi.middleware.trustedhost import TrustedHostMiddleware

app = FastAPI()

# allowed_hosts 不包括端口号,可以使用 `*` 通配符
app.add_middleware(TrustedHostMiddleware, allowed_hosts=["127.0.0.1", "example.com", "*.example.com"])

@app.get("/items")
async def read_items():
    return { "hello": "world"}

6.4 CORSMiddleware

CORSMiddleware 中间件用于处理 跨域资源共享(CORS)请求,允许浏览器客户端跨域请求。

源(Origin)」由协议、域名和端口组成,例如:https://example.com:8080,只有这三者完全相同的请求才是同源请求(相对于浏览器地址栏的地址)。同源请求不受浏览器的同源策略限制,可以自由访问。

CORS 是一种允许网页从不同域(源)请求资源的机制,通常用于在 JavaScript 代码中跨域 API 调用。例如:https://example.com/ 网页中通过 JavaScript 代码请求(Fetch/XHR) https://api.example.com/data.json 的数据,两者不同源。默认情况下在 https://example.com/ 网页中加载的 JavaScript 代码要想请求 https://api.example.com/data.json 链接,需要征得 https://api.example.com/data.json 服务端的同意。

出于安全性,浏览器限制了脚本内发起的跨源 HTTP 请求。跨域请求需要遵循 浏览器的同源策略。

CORS 请求分为 简单请求 和 预检请求:

  • 简单请求:使用 GET、POST 或 HEAD 方法,且头部为特定集合。简单请求直接正式发送,不需要预检请求。
  • 预检请求:复杂请求前,浏览器会先自动发送 OPTIONS 请求,确认服务器是否允许,允许后再发送正式请求。

CORS 请求相关头部:

  • 请求头部
    • Origin:请求的源(浏览器地址栏的源),告诉服务端该请求来自哪个源(简单请求和预检请求均包括)。
    • Access-Control-Request-Methods:预检请求的方法(仅用于预检请求)。
    • Access-Control-Request-Headers: 预检请求的头部(仅用于预检请求)。
  • 响应头部
    • Access-Control-Allow-Origin:允许的源(只能指定一个),* 表示所有。
    • Access-Control-Allow-Methods:允许的 HTTP 方法,多个之间使用英文逗号分隔,* 表示所有。
    • Access-Control-Allow-Headers:允许的头部,多个之间使用英文逗号分隔,* 表示所有。
    • Access-Control-Allow-Credentials:是否允许携带凭证,值仅为 true(区分大小写),如果要为 false 则不应包含该标头。凭据包括 Cookie、TLS客户端证书,或包含用户名和密码的认证标头。
    • Access-Control-Expose-Headers:可以暴露给浏览器中运行的脚本的头部,多个之间使用英文逗号分隔,* 表示所有。
    • Access-Control-Max-Age:预检请求的结果可以被缓存多久,单位为秒。

FastAPI 中的 CORSMiddleware 中间件就是用于处理 CORS 请求的合法性:

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# 添加 CORS 中间件
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost", "http://localhost:8080"],
    allow_methods=["*"],
    allow_headers=["*"],
    allow_credentials=True,
    expose_headers=["*"],
    max_age=600,
)

@app.get("/items")
async def read_items():
    return { "hello": "world" }

7. 依赖注入

「依赖注入」是声明代码中所需要的依赖项,然后由框架自动解析依赖项并传递给需要的函数。依赖注入可以减少代码的重复,提高代码的可维护性。

「依赖项」也称为组件(Component)、资源(Resource)、提供者(Provider)、服务(Service)、可注入项(Injectable) 等。

在 FastAPI 中依赖项是一个可调用对象,可以是同步/异步函数、类/对象、单例等,FastAPI 支持在路径操作函数中使用依赖注入。

7.1 依赖项函数

FastAPI 中最简单的依赖项就是函数,依赖项函数可以使用与路径操作函数相同的参数。

from fastapi import FastAPI, Path, Query, Depends

app = FastAPI()

async def get_item(item_id: str = Path(), q: str = Query(None)):
    """
    作为依赖项的函数,可以使用与路径操作函数相同的参数结构。
    """
    return {
        "item_id": item_id,
        "q": q,
    }

@app.get("/items/{item_id}")
async def read_item(item: dict = Depends(get_item)):
    """
    item 参数将是 get_item() 函数的返回值。
    """
    print(f"item_id={item["item_id"]}, q={item["q"]}")
    return item

上面代码中简单演示了依赖项函数的使用:

  • 依赖项函数可以是 async defdef 函数,FastAPI 知道如何正确使用它们。
  • 依赖项函数的形式和结构与路径操作函数一样,可以看做是没有装饰器的路径操作函数。
  • 依赖项函数的参数也可以继续包含其他依赖项,以此形式依赖树。FastAPI 会处理所有依赖项及其子依赖项,并为每一步操作提供(注入)结果。。
  • 路径操作函数的参数可以使用 Depends() 函数注入依赖项函数,Depends() 函数的参数必须是可调用对象。
  • 当客户端请求路径时,框架会自动调用依赖项函数,并把依赖项函数的返回值传递给路径操作函数的参数。

7.2 类作为依赖项

用于依赖注入的 Depends() 函数的参数是一个依赖项,依赖项只要是可调用对象即可,因此可以使用类作为依赖项。

类是可调用对象,类调用实际上调用的是类的构造方法(__init__()),并且返回一个类的实例。

from fastapi import FastAPI, Path, Query, Depends

app = FastAPI()

class ReqParams:
    def __init__(self, item_id: str = Path(), q: str = Query(None)):
        self.item_id = item_id
        self.q = q

@app.get("/items/{item_id}")
async def read_item(item: ReqParams = Depends(ReqParams)):
    """
    当请求路径时,FastAPI 会调用 `ReqParams()`,并传相关参数,然后返回类的实例赋值给依赖项参数。
    """
    print(f"item_id={item.item_id}, q={item.q}")
    return item

当使用类作为依赖项时,FastAPI 已明确知道依赖项参数的类类型,可以在 Depends() 函数中省略依赖项传递,上面的依赖项参数可以简写为:

item: ReqParams = Depends()

7.3 路径操作装饰器依赖项

有时候我们并不需要依赖项的返回值,只是想在路径操作函数执行前执行一些操作,可以在路径操作装饰器中添加一个 dependencies 参数,传递一组依赖项。

from fastapi import FastAPI, Header, Query, HTTPException, Depends

app = FastAPI()

async def verify_token(x_token: str = Header()):
    if x_token != "you_token":
        raise HTTPException(status_code=401, detail="invalid token")

def verify_key(key: str = Query()):
    if key != "you_key":
        raise HTTPException(status_code=401, detail="invalid key")

@app.get("/items", dependencies=[Depends(verify_token), Depends(verify_key)])
async def read_items():
    """
    请求路径时,FastAPI 会自动依次调用 `verify_token()` 和 `verify_key()` 函数,如果依赖项函数抛出异常,FastAPI 会返回异常响应。
    添加到装饰器的 dependencies 的装饰器,如果有返回值,将被忽略。
    """
    return { "hello": "world" }

7.4 子路由依赖项

以上示例代码中的依赖项都是添加到单个路径操作函数中,如果要为一组路径操作函数添加相同的依赖项,可以使用后面介绍的 APIRouter 子路由。先把路径操作函数添加到同一组路由中,然后再 app 包含路由时指定依赖项,或者在创建子路由实例时就指定依赖项。

from fastapi import FastAPI, APIRouter, Header, Query, HTTPException, Depends

app = FastAPI()

async def verify_token(x_token: str = Header()):
    if x_token != "you_token":
        raise HTTPException(status_code=401, detail="invalid token")

async def verify_key(key: str = Query()):
    if key != "you_key":
        raise HTTPException(status_code=401, detail="invalid key")

# 创建子路由时指定该路由下的所有路径都需要执行的依赖项
router = APIRouter(dependencies=[Depends(verify_key)])

# 在子路由中添加路径操作函数
@router.get("/items")
async def read_items():
    return { "hello": "world" }

# 将子路由添加到应用中时,也可以指定该路由下所有路径都需要执行的依赖项(此处添加的依赖项先执行)。
app.include_router(router, dependencies=[Depends(verify_token)])

7.5 全局依赖项

有时候我们需要为所有路径操作函数添加相同的依赖项,可以在创建 FastAPI 实例时就指定全局依赖项。

from fastapi import FastAPI, Header, HTTPException, Depends

async def verify_token(x_token: str = Header()):
    if x_token != "you_token":
        raise HTTPException(status_code=401, detail="invalid token")

app = FastAPI(dependencies=[Depends(verify_token)])

@app.get("/items")
async def read_items():
    return { "hello": "world" }

7.6 HTTP Basic Auth

依赖项可以在每次请求之前之前执行,可以在依赖项中实现 HTTP 基础授权(HTTP Basic Auth)。

FastAPI 提供了用于获取 HTTP Basic Auth 用户名和密码的依赖项,相关类:HTTPBasic, HTTPBasicCredentials

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBasic, HTTPBasicCredentials

app = FastAPI()

security = HTTPBasic()

async def verify_user(credentials: HTTPBasicCredentials = Depends(security)):
    """
    用于校验 HTTP Basic Auth 的用户名和密码的依赖项,依赖项中又有 HTTPBasicCredentials 子依赖项,用于获取用户名和密码。
    """
    # 校验通过则返回用户名,否则抛出异常。
    if credentials.username != "hello" or credentials.password != "world":
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username

@app.get("/items")
async def read_items(username: str = Depends(verify_user)):
    return { "username": username }

除了 HTTPBasic,FastAPI 还提供了 HTTPBearerHTTPDigestOAuth2 等,参考:安全性 - FastAPI、高级安全 - FastAPI。

8. 错误处理

对于常见的错误,FastAPI 均有默认的异常处理器,也可以自己手动创建错误处理器处理特定的错误,或者覆盖默认的错误处理器。

8.1 HTTPException

对于服务器内部错误、访问资源不存在、参数校验错误等,FastAPI 会自动返回错误状态的响应。如果需要在代码中手动返回错误响应,可以使用 HTTPException 类。

from fastapi import FastAPI, HTTPException, status

app = FastAPI()

@app.get("/items/{item_id}")
async def read_items(item_id: str):
    if item_id not in ["abc", "def"]:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Item not found",
            headers={"X-Error": "There goes my error"},
        )
    return {"item_id": item_id}

访问路径:

$ curl -i http://127.0.0.1:8000/items/xyz
HTTP/1.1 404 Not Found
content-type: application/json

{
  "detail": "Item not found"
}

触发 HTTPException 时,参数 detail 可以接收任何能转换为 JSON 的值,不仅限于 str。 还支持传递 dictlist 等数据结构。FastAPI 能自动处理这些数据,并将之转换为 JSON。

8.2 自定义异常处理器

对于某些特定的异常类,可以自定义异常处理器函数来处理。使用 @app.exception_handler() 装饰器添加异常处理器,异常处理器函数的参数是 请求对象 和 异常对象,返回一个 响应对象。

from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse

app = FastAPI()

class ItemNotFoundException(Exception):
    """
    自定义异常类
    """
    def __init__(self, item: str):
        self.item = item

@app.exception_handler(ItemNotFoundException)
async def item_not_found_exception_handler(request: Request, exc: ItemNotFoundException) -> Response:
    """
    自定义异常处理函数,当有类型为 ItemNotFoundException 的异常抛出时,会调用此函数。
    """
    return JSONResponse(
        status_code=200,
        content={
            "code": 404,
            "error": f"Item not found: {exc.item}"
        },
    )

@app.get("/items/{item_id}")
async def read_items(item_id: str):
    if item_id not in ["abc", "def"]:
        raise ItemNotFoundException(item_id)
    return {"item_id": item_id}

访问路径:

$ curl -i http://127.0.0.1:8000/items/xyz
HTTP/1.1 200 OK
content-type: application/json

{
  "code": 404,
  "error": "Item not found: xyz"
}

除了使用 @app.exception_handler() 装饰器把函数添加为异常处理器外,还可以使用 app.add_exception_handler() 方法添加异常处理器。

from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse

app = FastAPI()

class ItemNotFoundException(Exception):
    """
    自定义异常类
    """
    def __init__(self, item: str):
        self.item = item

async def item_not_found_exception_handler(request: Request, exc: ItemNotFoundException) -> Response:
    """
    自定义异常处理函数,当有类型为 ItemNotFoundException 的异常抛出时,会调用此函数。
    """
    return JSONResponse(
        status_code=200,
        content={
            "code": 404,
            "error": f"Item not found: {exc.item}"
        },
    )

# 注册自定义异常处理函数
# 第一个参数是自定义异常类,第二个参数是自定义异常处理函数。
# 异常处理函数可以是同步函数,也可以是异步函数。
# 处理 HTTP 请求的异常,异常处理函数的参数为 Request 和 异常对象。
# 处理 WebSocket 请求的异常,异常处理函数的参数为 WebSocket 和 异常对象。
app.add_exception_handler(ItemNotFoundException, item_not_found_exception_handler)

@app.get("/items/{item_id}")
async def read_items(item_id: str):
    if item_id not in ["abc", "def"]:
        raise ItemNotFoundException(item_id)
    return {"item_id": item_id}

除了对某个异常类添加处理器外,app.add_exception_handler() 方法还可以添加对某个异常状态码的处理器(覆盖默认的处理器)。

from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse

app = FastAPI()

async def not_found_exception_handler(request: Request, exc: Exception) -> Response:
    return JSONResponse(
        status_code=404,
        content={
            "code": 404,
            "error": f"页面没有找到: {exc}"
        },
    )

# 当状态码为 404 时,使用 not_found_exception_handler 异常处理器。
app.add_exception_handler(404, not_found_exception_handler)

8.3 覆盖默认的异常处理器

对于某些类型的错误,FastAPI 有默认的异常处理器,可以使用 @app.exception_handler() 装饰器覆盖默认的异常处理器。

例如当参数交易失败时,FastAPI 默认返回 422 状态码,可以使用 RequestValidationError 类型的异常处理器覆盖默认的异常处理器。

from fastapi import FastAPI, Request, Response
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError

app = FastAPI()

@app.exception_handler(RequestValidationError)
async def request_validation_exception_handler(request: Request, exc: RequestValidationError) -> Response:
    return JSONResponse(
        status_code=200,
        content={
            "code": 422,
            "error": jsonable_encoder(exc.errors())
        },
    )

@app.get("/items/{item_id}")
async def read_items(item_id: int):
    return {"item_id": item_id}

访问路径:

$ curl -i http://127.0.0.1:8000/items/xyz
HTTP/1.1 200 OK
content-type: application/json

{
  "code": 422,
  "error": [
    {
      "type": "int_parsing",
      "loc": ["path", "item_id"],
      "msg": "Input should be a valid integer, unable to parse string as an integer",
      "input": "xyz"
    }
  ]
}

9. 生命周期事件

FastAPI 的生命周期事件可以在应用启动时和关闭时执行一些操作,例如:初始化数据库连接、关闭数据库连接等。

你可以使用 FastAPI() 构造函数的 lifespan参数 和 一个异步上下文管理器注解 来定义 启动关闭 的逻辑。

from contextlib import asynccontextmanager
from fastapi import FastAPI

@asynccontextmanager
async def app_lifespan(app: FastAPI):
    try:
        print("APP UP")         # 应用启动时(处理请求前)执行的代码
        yield                   # 阻塞至应用关闭
    finally:
        print("APP DOWN")       # 应用关闭时执行的代码

# 指定应用的生命周期处理函数
app = FastAPI(lifespan=app_lifespan)

@app.get("/items")
async def read_items():
    return {"hello": "world"}

10. WebSockets 服务

FastAPI 支持 WebSocket 服务,可以使用 @app.websocket() 装饰器声明处理 WebSocket 请求的路径操作函数。

from fastapi import FastAPI, WebSocket

app = FastAPI()

@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
    print(f"client: {ws.client}")
    print(f"url: {ws.url}")
    print(f"headers: {ws.headers}")
    await ws.accept()
    while True:
        data = await ws.receive_text()
        await ws.send_text(f"Echo: {data}")

WebSocket 路径操作函数和普通的 HTTP 路径操作函数一样,也可以声明路径参数和查询参数。

from fastapi import FastAPI, WebSocket, Request

app = FastAPI()

@app.websocket("/ws/{item_id}")
async def websocket_endpoint(ws: WebSocket, item_id: str, q: str | None = None):
    print(f"item_id: {item_id}, q: {q}")
    await ws.accept()
    while True:
        data = await ws.receive_text()
        await ws.send_text(f"Echo: {data}")

11. APIRouter

开发一个完整的 Web API 服务时,可能会有很多路径操作函数,为了更好的组织代码,可以使用 APIRouter 类来创建一个子路由器,把相关联路径操作函数放到单独文件中。

可以像使用 FastAPI 实例一样使用 APIRouter 实例,包括添加路径操作函数、添加中间件、添加异常处理器等。添加到 FastAPI 实例的中间件和异常处理器是全局有效的,添加到 APIRouter 实例的中间件和异常处理器只对该路由器有效。

APIRouter 的使用示例,下面三个文件在同一目录下。

items.py 文件:

from fastapi import APIRouter

router = APIRouter(tags=["items"])      # tags 是在 OpenAPI 文档中的分组标签,没有标签默认为 default 组。

# 可以继续为 router 添加子路由
# router.include_router(...)

@router.get("/items/{item_id}")
async def read_item(item_id: str):
    """
    访问 `http://127.0.0.1:8000/items/{item_id}` 时(假设父级路由没有指定prefix),将执行此函数。
    """
    return {"item_id": item_id}

users.py 文件:

from fastapi import APIRouter

router = APIRouter(prefix="/users", tags=["users"])


@router.get("")
async def all_users():
    """
    访问 `http://127.0.0.1:8000/users` 时(假设父级路由没有指定prefix),将执行此函数。
    """
    return {"message": "all users"}

@router.get("/{user_id}")
async def read_user(user_id: str):
    """
    访问 `http://127.0.0.1:8000/users/{user_id}` 时(假设父级路由没有指定prefix),将执行此函数。
    """
    return {"user": user_id}

main.py 文件:

from fastapi import FastAPI
import items
import users

app = FastAPI()

app.include_router(items.router)    # 为 app 添加 items 路由(可以添加前缀)
app.include_router(users.router)    # 为 app 添加 users 路由(可以添加前缀)

@app.get("/")
async def home():
    return {"home": "home page"}

12. 子应用挂载

如果需要多个独立的 FastAPI 应用实例,拥有各自独立的 OpenAPI 与文档,可以创建一个主应用,然后挂在一个或多个子应用。

FastAPI 挂在子应用时指在特定路径下挂载完全独立的 FastAPI 实例,子应用的路径操作函数的路径会自动添加挂载路径前缀。

from fastapi import FastAPI

sub1 = FastAPI()

@sub1.get("/items")
async def sub1_items():
    return {"page": "sub1 items"}

sub2 = FastAPI()

@sub2.get("/items")
async def sub2_items():
    return {"page": "sub2 items"}

app = FastAPI()

app.mount("/sub1", sub1)    # 挂载 sub1 app 到 "/sub1" 路径下
app.mount("/sub2", sub2)    # 挂载 sub2 app 到 "/sub2" 路径下

访问路径:

$ curl http://127.0.0.1:8000/sub1/items
{
  "page": "sub1 items"
}

$ curl http://127.0.0.1:8000/sub2/items 
{
  "page": "sub2 items"
}

查询子应用的 OpenAPI 文档:

  • http://127.0.0.1:8000/sub1/docs
  • http://127.0.0.1:8000/sub2/docs

你可能感兴趣的:(Python,高级模块应用,python,fastapi)