Skip to content

FastAPI入门

更新: 2025/2/24 字数: 0 字 时长: 0 分钟

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

  • 极高性能:可与 NodeJS 和 Go 比肩的极高性能(归功与 Starlette 和 Pydantic),是最快的 Python Web 框架之一。
  • 高效开发:使代码重复最小化,通过不同的参数声明实现丰富功能,提高功能开发速度约 200% 至 300%。
  • 更少错误:减少约 40% 的人为(开发者)导致的 Bug。
  • 智能补全:极佳的编辑器支持,处处皆可自动补全,减少调试时间。
  • 简单易学:设计的易于使用和学习,阅读文档的时间更短。
  • 交互文档:生产可用级别的代码,还有自动生成的交互式文档。
  • 标准化:基于(并完全兼容)API 的相关开放标准:OpenAPI(以前被称为 Swagger)和 JSON Schema

image-20240716232615034

提醒

FastAPI 是站在前人肩膀上,集成了多种框架优点的新秀框架。它出现的比较晚,2018 年底才发布在 GitHub 上。广泛应用于当前各种前后端分离的项目开发,测试运维自动化以及微服务的场景中。

相关介绍

侧重点

首先,我们看看框架的名称 FastAPI,它是由两个单词组成,一个是 Fast 含义是快速的,代表其高性能的特点。另一个是 API 含义是数据接口,代表其应用方向。在目前,前后端分离为主流的市场下,API 的开发异常重要,这也是 FastAPI 同样作为小而精的 Web 框架,不同于 Flask 的特点:

  • Flask 比较平衡,它对于前后端分离和前后端不分离两种情况下的支持是相同的。
  • FastAPI 有侧重点,它更偏向于 API 数据接口开发,所以说它是一个更现代的 Web 框架,因为它更适配于当前前后端分离的主流市场。

img

高性能

了解过 Python 语言的朋友都知道 Python 语言有一个弱点,就是并发能力弱,这主要是源于它的 GIL 锁,限制了多核环境下的线程并发,这个问题随着时间的推移愈发严重。近些年 Python 对于该问题的解决思路就是,基于协程异步的方式来实现性能的提升,反应到 Web 方向就出现了像 Tornado、Sanic 这样的 Web 框架,但它们都是小范围的应用,对于异步的支持也不够完善和友好。直到 FastAPI 的出现才真正将 Web 的异步发挥到了极致,这也是 FastAPI 能有极高性能的主要原因。性能测试参考:

img

核心库

FastAPI 是建立在 Pydantic 和 Starlette 这两个核心库基础之上的,具体说明如下:

  • Pydantic 是一个基于 Python 类型提示来定义数据验证、序列化和文档的库。在 FastAPI 中主要负责数据部分(类型提示)。

image-20240718230732022

  • Starlette 是一种轻量级的 ASGI 框架/工具包,是构建高性能 Asyncio 服务的理想选择。在 FastAPI 中主要负责 Web 部分(Asyncio 异步)。

image-20240718002019567

服务器

前面我们学习 Django、Flask 时,经常提及一个叫 WSGI 的协议,所谓 WSGI(Web Server Gateway Interface,Web 服务器网关接口)就是为 Python 定义的 Web 服务器和 Web 应用程序或框架之间的一种简单而通用的接口。因为 WSGI 应用都是一个单调用、同步接口,即输入一个请求,返回一个响应,所以我们可以把 WSGI 理解为一个同步的 Web 服务协议

随着移动网络的发展,出现了比如 WebSocket、HTTP/2、HTTP/3 等新的 Web 技术,这时 WSGI 开始暴露出许多限制,例如 WSGI 无法支持长连接或者 WebSocket 这样的连接,还有 WSGI 规定一个 URL 对应一个请求,所以也无法支持 WebSocket、HTTP/2 等在一个 URL 里会出现多个请求的协议

在 Python 3.5+ 增加 async/await 特性之后,Python 的异步编程变得异常火爆,但 Python 仍缺乏用于 asyncio 框架最低限度的低级服务器/应用程序接口,而 ASGI 协议规范的出现填补了这一空白。所谓 ASGI(Asynchronous Server Gateway Interface,异步服务器网关接口)可以看作为 WSGI 的扩展,支持当前 Web 开发中的一些新的协议标准,旨在提供支持异步的 Python Web 服务器、框架和应用程序之间的标准接口。可以说,ASGI 为异步和同步应用程序提供了一个标准,成为了 Web 服务器、框架和应用程序之间的标准兼容性,而我们这里学习的 Starlette 使用的就是 ASGI 协议

快速上手

上面提到 FastAPI 的核心库之一是 Starlette 库 ,它使用到了 Asyncio 异步,而 Python 支持 Asyncio 异步是从 3.5 以后开始支持的,因此在安装 FastAPI 时,必须依赖 Python 3.6 及更高版本。执行下面命令查看 Python 版本:

python --version

image-20240718002920189

项目搭建

首先,选择一个路径,新建一个名称为 python_fastapi 的文件夹,命令如下:

# 在当前目录下新建名称为python_fastapi的文件夹
mkdir python_fastapi

接着,以 python_fastapi 文件夹为项目的根目录,搭建名称为 venv 的新虚拟环境,执行命令如下:

# 进入python_fastapi文件夹
cd python_fastapi

# 搭建名称为venv的新虚拟环境
virtualenv venv

image-20240718230315549

现在,新项目的虚拟环境就搭建好了,我们在 PyCharm 打开新项目并将 venv 虚拟环境作为项目的执行环境,当我们再次打开 PyCharm 中的 Terminal 时,就会发现新环境已经启用了:

image-20240718232242722

接着,我们在 Terminal 中执行下面命令来安装 FastAPI 框架:

pip install fastapi==0.111.1

然后,我们还需安装一个叫 uvicorn 的东西,它是一个基于 uvloophttptools 构建的支持异步多进程的 ASGI 服务器,同时也是是运行 FastAPI 应用程序的主要 Web 服务器,安装命令如下:

pip install uvicorn==0.18.1

警告

目前 uvicorn 最新的 0.30.1 版本没有 Debug 参数,用不了调试功能,因此这里我们降低版本至 0.18.1 进行安装。

最后,为了保证项目能正常运行,我们在 python_fastapi 项目里新建一个名称为 example 的 Python 包,后面我们学习的案例都会放到这个 example 包当中:

::: image-group

<1>

<2>

<3>

:::

简单案例

example 包里新建一个名称为 example01.py 文件,写入代码如下:

image-20240721011828531

python
# FastAPI是整个fastapi框架的核心类
from fastapi import FastAPI

# 将FastAPI类实例化为app,这个app将是创建你所有API的主要交互对象
app = FastAPI()

# 使用app装饰器装饰home异步函数,请求方法是GET,路由是/
@app.get(path='/')
async def home():
    return {"message": "Welcome to home page!"}

# 使用app装饰器装饰shop异步函数,请求方法是GET,路由是/shop
@app.get(path='/shop')
async def shop():
    return {"message": "Welcome shopping!"}

if __name__ == '__main__':
    uvicorn.run(
        app="example01:app",  # FastAPI实例对象
        host="127.0.0.1",     # 主机,默认127.0.0.1
        port=5000,            # 端口,默认8000
        reload=True,          # 热更新,有内容修改自动重启服务器
        debug=True,           # 同reload
        log_level="info"      # 日志级别,默认info
    )

运行代码后,uvicorn 服务器会被启动起来,注意由于 uvicorn 的版本问题,启动后可能不会有任何提示(运行的主机、端口信息)。不过我们访问 uvicorn.run 中设定的主机、端口以及代码中设定的路由,是能看到对应的页面:

::: image-group

<1>

<2>

:::

建议

如果有小伙伴想通过命令行启动 uvicorn 服务器,就需要在项目根路径下执行 uvicorn 包.文件名:FastAPI实例对象 --reload 命令,对应到当前项目就是 uvicorn example.example01:app --reload 命令。命令启动全部使用默认配置,也就是访问的 URL 开头为 http://127.0.0.1:8000 这部分。

交互文档

使用 FastAPI 最酷的一点就是它会自动化生成接口的交互文档,这个交互文档放在了 http://主机:端口/docs 下,它的查看和使用方法如下:

  1. 访问本机 5000 端口的 /docs 可以看到如下界面,这里展示了两个使用 GET 请求的接口,这两个接口后的 //shop 所对应的就是代码中的两个路由,最后面的 HomeShop 所对应的就是代码中的两个函数名,只是首字母进行了大写
  2. 点击第一个接口,在弹出的下拉框中就能看到该接口的详细信息。继续点击图中的 Try it out 测试按钮,进入到下一步
  3. 这里不用进行任何的配置修改。点击图中的 Execute 执行按钮,它会向 uvicorn 服务器的当前接口发送一个 GET 请求
  4. 发送了 GET 请求后,交互文档会接收 uvicorn 服务器返回的响应,并展示出具体的响应头和响应体

::: image-group

<1>

<2>

<3>

<4>

:::

提醒

其他接口的操作和测试流程都是一样的,这里就不一一展示了。

FastAPI路径查询

更新: 2025/2/24 字数: 0 字 时长: 0 分钟

路径操作

FastAPI 路径操作包含两大部分:路径操作装饰器、Include_router。

路径操作装饰器

在我们项目中的每个函数前面都有一个路径操作装饰器,也就是代码中 @FastAPI实例对象.请求方式(path='路由') 这一部分,它的作用就是定义路由和请求方式

请求方式

FastAPI 支持各种请求方式,具体如下:

python
@app.get()      # 查看
@app.post()     # 添加
@app.put()      # 更新
@app.patch()    # 更新
@app.delete()   # 删除
@app.options()
@app.head()
@app.trace()

接着之前的项目,我们在 example 包中新建一个 example02.py 文件,写入如下代码:

image-20240721135253785

python
from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get('/get')
async def get_test():
    return {"method": "GET方法"}

@app.post('/post')
async def post_test():
    return {"method": "POST方法"}

@app.put('/put')
async def put_test():
    return {"method": "PUT方法"}

@app.delete('/delete')
async def delete_test():
    return {"method": "DELETE方法"}

if __name__ == '__main__':
    uvicorn.run(
        app="example02:app",  # FastAPI实例对象
        host="127.0.0.1",     # 主机,默认127.0.0.1
        port=5000,            # 端口,默认8080
        reload=True,          # 热更新,有内容修改自动重启服务器
        debug=True,           # 同reload
        log_level="info"      # 日志级别,默认info
    )

运行代码后,我们访问本机 5000 端口的 /docs 交互文档,就能看到如下界面:代码中的四个不同的请求函数就对应这里展示的四个接口文档,任何一个接口都能进行发送请求的测试。

image-20240721135436832

方法参数

在路径操作装饰器中,我们可以添加一些参数来自定义交互文档所展示的内容。接着之前的项目,我们在 example 包中新建一个 example03.py 文件,写入如下代码:

image-20240721141518614

python
from fastapi import FastAPI
import uvicorn

app = FastAPI()

@app.get(
    path='/get',                                          # 路由
    tags=['GET请求的tags'],                                # 标签
    summary='GET请求的summary',                            # 概述
    description='GET请求的description',                    # 描述
    response_description='GET请求的response_description',  # 响应的描述
    deprecated=True                                       # 废弃当前接口
)
async def get_test():
    return {"method": "GET方法"}

if __name__ == '__main__':
    uvicorn.run(
        app="example03:app",  # FastAPI实例对象
        host="127.0.0.1",     # 主机,默认127.0.0.1
        port=5000,            # 端口,默认8080
        reload=True,          # 热更新,有内容修改自动重启服务器
        debug=True,           # 同reload
        log_level="info"      # 日志级别,默认info
    )

运行代码后,我们访问本机 5000 端口的 /docs 交互文档,就能看一个接口文档,展开后界面如下:由于设置 deprecated=True 当前接口被废弃,因此显示的颜色是灰色

image-20240721142131051

Include_router

目前我们项目中所有的路由都是在一个文件中的,而且是也都是直接挂载到全局路由 app 下的。虽然这样写很方便,但会导致一个隐患,就是项目变大以后,路由就很不好管理。因此在大型项目中,通常会有多个子应用,而 Include_router 就负责将不同子应用的路由做一个分发和解耦

场景模拟

接着前面的项目,为了模拟多个子应用的场景,我们在 example 包中,新建一个 apps 包,在 apps 包中新建 app01 包、app02 包,在 app01 包、app02 包里面分别再新建一个名称为 urls.py 的路由映射文件,写入如下代码:

::: image-group

app01rls.py

app02rls.py

:::

python
# 从fastapi中导入APIRouter接口路由类
from fastapi import APIRouter

# 实例化为user对象
user = APIRouter()

# 注册函数
@user.get(path='/register')
async def register():
    return {'user': 'register'}

# 登录函数
@user.get(path='/login')
async def login():
    return {'user': 'login'}
python
# 从fastapi中导入APIRouter接口路由类
from fastapi import APIRouter

# 实例化为shop对象
shop = APIRouter()

# 食物商品
@shop.get(path='/food')
async def food():
    return {'shop': 'food'}

# 床上用品
@shop.get(path='/bed')
async def bed():
    return {'shop': 'bed'}

注册路由

现在,我们就需要将 app01 包、app02 包中的子路由注册到 app 主路由上。在 example 包中新建 example04.py 文件,写入如下代码:

image-20240721162547899

python
from fastapi import FastAPI
import uvicorn
# 引入子应用app01里面的user接口路由对象
from apps.app01.urls import user
# 引入子应用app02里面的shop接口路由对象
from apps.app02.urls import shop

# 实例化的全局路由app
app = FastAPI()
# 将子路由user注册全局路由app中,prefix前缀为"/user",tags标签为"用户接口"
app.include_router(router=user, prefix="/user", tags=["用户接口"])
# 将子路由shop注册全局路由app中,prefix前缀为"/shop",tags标签为"购物接口"
app.include_router(router=shop, prefix="/shop", tags=["购物接口"])

if __name__ == '__main__':
    uvicorn.run(
        app="example04:app",  # FastAPI实例对象
        host="127.0.0.1",     # 主机,默认127.0.0.1
        port=5000,            # 端口,默认8080
        reload=True,          # 热更新,有内容修改自动重启服务器
        debug=True,           # 同reload
        log_level="info"      # 日志级别,默认info
    )

运行代码后,我们访问本机 5000 端口的 /docs 交互文档,就能看到如下界面:FastAPI 按子应用的路由和标签整理好了接口文档,这对我们后期管理项目路由非常有帮助

image-20240721162736501

路径参数

目前,我们项目中所有的函数路由都是写死的,也就是说请求的路由和返回的内容之间是固定。假如我们需要根据路由的参数来返回对应的内容,就必须要用到路径参数。

传递动态参数

我们在 example 包中新建一个 son 包,并在里面新建一个 son01.py 文件,写入如下代码:

image-20240722231814955

python
# 从fastapi中导入APIRouter接口路由类
from fastapi import APIRouter

son01 = APIRouter()

@son01.get(path='/user/1')
async def get_user():
    return {"user_id": "1"}

继续在 example 包中新建一个 example05.py 文件,写入如下代码:

image-20240722232042449

python
from fastapi import FastAPI
import uvicorn
from son.son01 import son01

# 实例化的全局路由app
app = FastAPI()
# 将子路由son01注册全局路由app中
app.include_router(router=son01, tags=["01 路径参数"])

if __name__ == '__main__':
    uvicorn.run(
        app="example05:app",  # FastAPI实例对象
        host="127.0.0.1",     # 主机,默认127.0.0.1
        port=5000,            # 端口,默认8080
        reload=True,          # 热更新,有内容修改自动重启服务器
        debug=True,           # 同reload
        log_level="info"      # 日志级别,默认info
    )

运行代码后,我们访问本机 5000 端口的 /docs 交互文档,就能看到 /user/1 的接口文档生成了,具体如下:

  1. 这个接口 /user/1 没有任何参数,点击 Try it out 按钮进行测试。
  2. 点击 Execute 执行按钮,向服务器发送请求。
  3. 请求 URL 为本机 5000 端口的 /user/1,响应内容为 {"user_id": "1"}

::: image-group

<1>

<2>

<3>

:::

可以看到接口 /user/1 没有任何参数,返回的内容也永远是 {"user_id": "1"},缺乏灵活性。假如我们需要接口根据参数来返回对应的结果,就需要修改 son01.py 文件的代码,具体如下:

image-20240722233942199

python
# 从fastapi中导入APIRouter接口路由类
from fastapi import APIRouter

son01 = APIRouter()

# {}中的uid就是接口接收动态参数的变量,函数形参也必须是同名uid变量
@son01.get(path='/user/{uid}')
async def get_user(uid):
    return {"user_id": uid}

服务器重启后,我们再去访问本机 5000 端口的 /docs 交互文档,就能看到 /user/{uid} 的接口文档有点不一样了,具体如下:

  1. 之前 /user/1 的接口文档是没参数的,展开 /user/{uid} 的接口文档后,现在多了一个 uid 参数,点击 Try it out 按钮进行测试。
  2. uid 参数后面的输入框中输入 999,点击 Execute 执行按钮,向服务器发送请求。
  3. 这时请求 URL 变为本机 5000 端口的 /user/999,响应内容为 {"user_id": "999"},从而实现了路径的动态参数传递。

::: image-group

<1>

<2>

<3>

:::

建议

现在很多网站的 URL 中都有动态参数,这些动态参数所关联的就是当前网页,只要参数一变,网页也会跟着变。

参数类型限制

这里可能有细心的小伙伴观察到了一个细节,就是上面例子中,接口接受的参数是 999,在返回结果时变成字符串 "999",这说明传送给路由函数的任何参数都默认为是字符串。假如,现在我们需要将响应结果的类型限制为整型,该怎么做呢?很简单,使用 Python 3.6+ 提供的类型声明即可,我们在路由函数的形参后面加上类型声明,具体代码如下:

image-20240723230108390

python
# 从fastapi中导入APIRouter接口路由类
from fastapi import APIRouter

son01 = APIRouter()

# {}中的uid就是接口接收动态参数的变量,函数形参也必须是同名uid变量
@son01.get(path='/user/{uid}')
async def get_user(uid: int):  # 注释:限制uid变量为int类型
    return {"user_id": uid}

服务器重启后,我们再去访问本机 5000 端口的 /docs 交互文档,在 /user/{uid} 的接口上再发送一次请求,就会发返回的结果是整型的 999 了:这说明传送给路由函数的 uid 参数进行了一次类型转换,将字符串类型的参数转换为了整型参数

image-20240723230313370

如果传送给路由函数的 uid 参数不全是数字,那么接口文档在进行类型检查时,就会返回如下错误信息:要求 uid 参数的值必须为一个整数。

image-20240723231247138

建议

uid 参数下面有一个 integer 标签,就表明了该参数接收的值必须为一个整数。

路由匹配顺序

son01.py 文件中存放着我们的路由函数,假如说我们在 /user/{uid} 路由函数前面加上一个 /user/1 路由函数,具体代码如下:

image-20240723232216677

python
# 从fastapi中导入APIRouter接口路由类
from fastapi import APIRouter

son01 = APIRouter()

# 声明一个固定的'/user/1'路由
@son01.get(path='/user/1')
async def get_user():
    return {"user_id": "root"}  # 注释:返回固定的root内容

# {}中的uid就是接口接收动态参数的变量,函数形参也必须是同名uid变量
@son01.get(path='/user/{uid}')
async def get_user(uid: int):  # 注释:限制uid变量为int类型
    return {"user_id": uid}

服务器重启后,我们再去访问本机 5000 端口的 /docs 交互文档,然后进行如下测试:

  1. 首先,测试 /user/1 接口,结果返回 {"user_id": "root"}

  2. 然后,测试 /user/{uid} 接口,参数为 999,结果返回 {"user_id": 999}

  3. 最后,测试 /user/{uid} 接口,参数为 1,结果返回 {"user_id": "root"}

::: image-group

<1>

<2>

<3>

:::

在测试的案例中,第一次和第三次测试返回了相同的结果,这说明请求的 URL 会按照路由在文件中的先后顺序进行匹配,若前面的路由匹配成功后,则后面的路由不再进行匹配,因此后面的路由函数也不会执行。同样的,如果我们将 /user/{uid} 路由函数放置到 /user/1 路由函数的前面,那么 /user/1 路由将会被 /user/{uid} 路由所覆盖,所返回的结果就不再是 {"user_id": "root"},而是 {"user_id": 1},测试结果如下:

image-20240724000401181

查询参数

上面我们学习了路径参数,现在我们来学习查询参数,所谓的“查询参数”就是在 URL 中的查询条件。现在很多网站都具备条件查询功能,以 BOSS 直聘为例,我这里选择了如下几个条件:

image-20240725000807270

选择好条件以后,就可以看到 URL 中多了一些参数,完整的 URL 为 https://www.zhipin.com/web/geek/job?query=python&city=101270100&experience=105,其中 ? 后面的 query=python&city=101270100&experience=105 这部分就是查询参数,我们按 & 符进行分割拆解如下:

  • query=python 参数:query 意为查询,python 就是我们输入的搜索词。
  • city=101270100 参数:city 意为城市,101270100 就是我们选择的城市的编号。
  • experience=105 参数:experience 意为经验,105 就是我们选择的工作经验年限的编号。

查询条件参数

现在,我们在 FastAPI 中来实现上述条件查询参数。在项目的 son 包中新建一个 son02.py 文件,写入如下代码:

image-20240727145946617

python
# 从fastapi中导入APIRouter接口路由类
from fastapi import APIRouter

son02 = APIRouter()

# 路由函数的形参中,不属于路由的其他函数时,将被自动解释为“查询条件参数”
@son02.get(path='/jobs')
async def jobs(query, city, experience):
    return {
        'query': query,
        'city': city,
        'experience': experience
    }

我们可以看到jobs 路由函数中有 querycityexperience 三个形参,它们都没有出现在 path 路径参数中,因此它们被自动解释为“查询条件参数”。接着我们再去到 example05.py 文件中,写入下面的代码:

image-20240727150635328

python
from son.son02 import son02

# 将子路由son02注册全局路由app中
app.include_router(router=son02, tags=["02 查询参数"])

运行代码后,我们访问本机 5000 端口的 /docs 交互文档,就能看到 /jobs 的接口文档生成了,具体测试操作如下:

  1. 展开 /jobs 接口文档,可以看到里面就有 jobs 路由函数中的三个形参,其中 *required 表明参数是必填项。
  2. 点击 Try it out 按钮,分别填入如下参数,最后点击 Execute 按钮向服务器发起请求。
  3. 在服务器的响应中我们可以看到,请求的 URL 中 ? 后面的 query=python&city=%E6%88%90%E9%83%BD&experience=3~5%E5%B9%B4 这部分就是查询参数(汉字会自动转为带有 % 符的 URL 编码)。在 Response body 响应体里面返回的就是我们上面传入的参数。

::: image-group

<1>

<2>

<3>

:::

参数可填设置

观察上面的案例,会发现不合理的一点是,接口中的所有查询参数都是必填项。但在 BOSS 直聘的查询条件中,只有一个搜索关键词是必填项,而城市、工作经验这两个查询条件是非必填项,这就是我们接下来要解决的问题。我们希望将必填项作为路径参数,将非必填项作为查询参数,修改 son02.py 文件,修改代码如下两点:

  1. query 形参添加到 path 路由中变为路径参数,而 cityexperience 仍是查询参数。
  2. cityexperience 设置默认参数 None,表示这两个参数可以传参,也可以不传参

image-20240727172701179

python
# 从fastapi中导入APIRouter接口路由类
from fastapi import APIRouter

son02 = APIRouter()

# 在path添加query形参,query就变为路径参数,而city、experience仍是查询参数。
@son02.get(path='/jobs/{query}')
# 给city、experience设置默认参数None,表示这两个参数可以传参,也可以不传参。
async def jobs(query, city=None, experience=None):
    return {
        'query': query,
        'city': city,
        'experience': experience
    }

服务器重启后,我们再去访问本机 5000 端口的 /docs 交互文档,然后进行如下测试:

  1. 展开 /jobs 接口文档,可以看到里面仍有三个原先的参数,但其中只有 query 参数是 *required 必填项,而且是 path 路径参数。其余两个 cityexperience 参数没有 *required 就表示非必填项,而且是 query 查询参数
  2. 点击 Try it out 按钮,我们只填入 query 路径参数和一个 city 查询参数,最后点击 Execute 按钮向服务器发起请求。
  3. 在服务器的响应中我们可以看到,请求的 URL 发生了变化,其中填入的 query 参数出现在了 URL 的路径中,填入的 city 参数出现在了 URL 的查询中。再看 Response body 响应体里面返回的内容,querycity 都有值,而 experience 参数没有填值,就默认返回了一个 null

::: image-group

<1>

<2>

<3>

:::

可填类型设置

前面我们学习了限制参数必须为某一个类型,可以和这里的参数可填设置配合使用。这里我们将 city 设置为只能接收 int 整型,但又可以为 None 空的形式,修改 son02.py 文件,添加如下代码:

image-20240728151952266

python
# 从fastapi中导入APIRouter接口路由类
from fastapi import APIRouter

son02 = APIRouter()

@son02.get(path='/jobs/{query}')
# city: int = None,表示city只能接收整型,默认值None。
async def jobs(query, city: int = None, experience=None):
    return {
        'query': query,
        'city': city,
        'experience': experience
    }

服务器重启后,我们再去访问本机 5000 端口的 /docs 交互文档,然后进行如下测试:

  1. 展开 /jobs 接口文档,可以看到里面仍有三个原先的参数,其中 city 参数没有 *required 就表示非必填项,而且是 integer 表示只能接收整型数字
  2. 点击 Try it out 按钮,我们只填入 query 参数和一个 city 参数,最后点击 Execute 按钮向服务器发起请求。
  3. 在服务器的响应中我们可以看到,请求的 URL 发生了变化,其中填入的 query 参数出现在了 URL 的路径中,填入的 city 参数出现在了 URL 的查询中。再看 Response body 响应体里面返回的内容,query 值是 python 字符,city 值是 610000 整型,而 experience 参数没有填值,就默认返回了一个 null

::: image-group

<1>

<2>

<3>

:::

FastAPI请求体数据

更新: 2025/2/24 字数: 0 字 时长: 0 分钟

前面我们学习了路径参数和查询参数,它们作为 URL 的一部分直接暴露在 URL 中是十分不安全的,而且路径参数和查询参数的长度都存在限制。基于这两点原因,在向服务器发起请求的时候,我们更希望把数据放在请求体中,因此本章节的内容十分的重要。

简单使用

前面我们介绍 FastAPI 时,提到 FastAPI 是建立在 Pydantic 和 Starlette 这两个核心库基础之上的,其中这个 Pydantic 负责数据的强制校验,对于不符合类型要求的就会抛出异常

提醒

安装 FastAPI 时,通常会顺带安装 Pydantic 库。如果没有安装,请通过 pip install pydantic==2.8.2 命令安装。

常用类型

这里,我们先来学习如何使用 Pydantic 进行数据校验。首先创建一个 User 类,这个类继承 Pydantic 中的 BaseModel 基础模型类,在 User 类中定义每个字段的类型限制以及参数可填设置,使用的语法和 FastAPI 中的路由函数中的参数类型限制、参数可填设置一样。代码如下

python
# 从pydantic中导入BaseModel基础模型类
from pydantic import BaseModel

class User(BaseModel):
    """User类继承BaseModel类"""
    uid: int       # 注释:uid需要一个int类型,必须字段。
    name: str      # 注释:name需要一个str类型,必须字段。
    age: int = 18  # 注释:age需要一个int类型,非必须字段,默认值是18。
  1. 标准数据案例:创建一个字典,里面加入 User 类所有必须字段以及相应的值,然后解包发送给 User 类进行数据校验,校验数据通过以后,返回 user 对象,通过 对象.字段 获取对应的值。
  2. 自动类型转换:当数据中的字段类型和类中的字段所限制的类型不同时,会自动将数据中的字段类型转换为类中的字段所限制的类型。
  3. 类型转换错误:当数据中的字段类型无法转换为类中的字段所限制的类型时,会抛出类型转换错误。
  4. 字段缺失错误:当数据中缺少类中的必须字段时,会抛出参数缺少错误。
python
# 标准数据(每个键的值都符合User类中字段的定义)
data = {
    'uid': 1,
    'name': 'Tim',
    'age': 20
}
user = User(**data)  # 注释:**data解包data里面的数据。
print(user)          # 输出:uid=1 name='Tim' age=20
print(user.uid)      # 输出:1
print(user.name)     # 输出:Tim
print(user.age)      # 输出:20
print(user.dict())   # 输出:{'uid': 1, 'name': 'Tim', 'age': 20}
python
# 类型不同(uid、age字段在User类中定义为int类型,这里给的是str类型)
data = {
    'uid': '2',
    'name': 'Tom',
    'age': '30'
}
user = User(**data)  # 注释:**data解包data里面的数据。
print(user)          # 输出:uid=2 name='Tom' age=30
print(user.uid)      # 输出:2
print(user.name)     # 输出:Tom
print(user.age)      # 输出:30
print(user.dict())   # 输出:{'uid': 2, 'name': 'Tom', 'age': 30}
python
# 转换错误(age字段在User类中定义为int类型,这里给的是str类型,且不能转换为int类型)
data = {
    'uid': '3',
    'name': 'Tam',
    'age': 'forty'
}
user = User(**data)  # 报错:age字段应该输入整型, 无法将字符串解析为整数。
python
# 字段缺乏(uid字段在User类中是必须字段,而这里没有uid字段)
data = {
    'name': 'Tcm',
}
user = User(**data)  # 报错:数据中必须要有uid字段。

其他类型

除了可以验证常用的 int 整型、str 字符型以外,我们 Pydantic 还可以 list 列表、date 日期时间类型。代码如下:

python
# 从pydantic中导入BaseModel基础模型类
from pydantic import BaseModel
# 从typing导入List和Optional
from typing import List, Optional
# 从datetime中导入datetime
from datetime import datetime

class User(BaseModel):
    """User类继承BaseModel类"""
    name: str                         # 注释:name需要一个str类型,必须字段。
    birth: Optional[datetime] = None  # 注释:birth需要一个datetime类型,非必须字段,默认值为None。
    hobby_list: List[int] = []        # 注释:hobby_list需要一个list类型,且list里面是int类型,非必须字段,默认值为[]。
  1. 标准数据案例:创建一个字典,里面加入 User 类所有必须字段以及相应的值,然后解包发送给 User 类进行数据校验,校验数据通过以后,返回 user 对象,通过 对象.字段 获取对应的值。
  2. 自动类型转换:当数据中的字段类型和类中的字段所限制的类型不同时,会自动将数据中的字段类型转换为类中的字段所限制的类型。
  3. 类型转换错误:当数据中的字段类型无法转换为类中的字段所限制的类型时,会抛出类型转换错误。
  4. 字段缺失错误:当数据中缺少类中的必须字段时,会抛出参数缺少错误。
python
# 标准数据(每个键的值都符合User类中字段的定义)
data = {
    'name': 'Tim',
    'birth': datetime(2000, 1, 1, 0, 0, 0),
    'hobby_list': [1, 2, 3]
}
user = User(**data)     # 注释:**data解包data里面的数据。
print(user)             # 输出:name='Tim' birth=datetime.datetime(2000, 1, 1, 0, 0) hobby_list=[1, 2, 3]
print(user.name)        # 输出:Tim
print(user.birth)       # 输出:2000-01-01 00:00:00
print(user.hobby_list)  # 输出:[1, 2, 3]
print(user.dict())      # 输出:{'name': 'Tim', 'birth': datetime.datetime(2000, 1, 1, 0, 0), 'hobby_list': [1, 2, 3]}
python
# 类型不同(hobby_list字段在User类中定义为list类型,里面为int类型,这里给的list类型中有str类型)
data = {
    'name': 'Tom',
    'birth': datetime(2020, 8, 9, 12, 0, 0),
    'hobby_list': [4, '5', '6']
}
user = User(**data)     # 注释:**data解包data里面的数据。
print(user)             # 输出:name='Tom' birth=datetime.datetime(2020, 8, 9, 12, 0) hobby_list=[4, 5, 6]
print(user.name)        # 输出:Tom
print(user.birth)       # 输出:2020-08-09 12:00:00
print(user.hobby_list)  # 输出:[4, 5, 6]
print(user.dict())      # 输出:{'name': 'Tom', 'birth': datetime.datetime(2020, 8, 9, 12, 0), 'hobby_list': [4, 5, 6]}
python
# 转换错误(name字段在User类中定义为str类型,这里给的是int类型,且不能转换为str类型)
data = {
    'name': 0,
}
user = User(**data)  # 报错:name字段应该输入一个str类型。
python
# 字段缺乏(name字段在User类中是必须字段,而这里没有name字段)
data = {}
user = User(**data)  # 报错:数据中必须要有name字段。

范围约束

Pydantic 不仅可以校验字段类型,还可以校验字段的值。这里我们给 age 字段限定一个 0 ~ 100 的范围,代码如下:

python
from pydantic import BaseModel, Field

class User(BaseModel):
    """User类继承BaseModel类"""
    name: str                                  # 注释:name需要一个str类型,必须字段。
    age: int = Field(default=0, gt=0, lt=100)  # 注释:age需要一个int类型,大小在(0, 100)之间,默认值为0,非必须字段。
  1. 标准数据案例:创建一个字典,里面加入 User 类所有必须字段以及相应的值,然后解包发送给 User 类进行数据校验,校验数据通过以后,返回 user 对象,通过 对象.字段 获取对应的值。
  2. 超出范围报错:当字段值不在 User 类中对应字段所限定值的范围时,会抛出错误。
python
# 标准数据(每个键的值都符合User类中字段的定义)
data = {
    'name': 'Tim',
    'age': 20
}
user = User(**data)  # 注释:**data解包data里面的数据。
print(user)          # 输出:name='Tim' age=20
print(user.name)     # 输出:Tim
print(user.age)      # 输出:20
print(user.dict())   # 输出:{'name': 'Tim', 'age': 20}
python
# 超出范围(age字段在User类中定义的范围是(0,100),这里给的0不在范围内)
data = {
    'name': 'Tom',
    'age': 0
}
user = User(**data)  # 报错:age字段值应该大于0。

内容检查

正则匹配

Pydantic 不仅可以校验字段类型、字段的值以外,还可以检查内容是否合规。这里我们检查 name 字段的值是否以 T 为开头,代码如下:

python
# 从pydantic中导入BaseModel基础模型类、Field字段类
from pydantic import BaseModel, Field

class User(BaseModel):
    """User类继承BaseModel类"""
    name: str = Field(pattern='^T')  # 注释:name需要一个str类型,必须以T为开头,必须字段。
  1. 标准数据案例:创建一个字典,里面加入 User 类所有必须字段以及相应的值,然后解包发送给 User 类进行数据校验,校验数据通过以后,返回 user 对象,通过 对象.字段 获取对应的值。
  2. 不符规范报错:当字段值不符合 User 类中对应字段所定义的规范时,会抛出错误。
python
# 标准数据(每个键的值都符合User类中字段的定义)
data = {
    'name': 'Tim',
}
user = User(**data)  # 注释:**data解包data里面的数据。
print(user)          # 输出:name='Tim'
print(user.name)     # 输出:Tim
print(user.dict())   # 输出:{'name': 'Tim'}
python
# 不符规范(name字段在User类中定义必须以T为开头,这里给的Aim不符合规范)
data = {
    'name': 'Aim',
}
user = User(**data)  # 报错:name字段值必须以T为开头。

装饰函数

Pydantic 还可以使用装饰器配合函数来校验字段的值。这里我们检查 name 字段的值是否全部都是字母,代码如下:

python
# 从pydantic中导入BaseModel基础模型类、field_validator装饰器
from pydantic import BaseModel, field_validator

class User(BaseModel):
    """User类继承BaseModel类"""
    name: str  # 注释:name需要一个str类型,必须字段。

    # field_validator装饰器检查name字段
    @field_validator("name")
    # cls表示当前类,value表示name字段的值
    def name_is_alpha(cls, value):
        assert value.isalpha(), "name mast be alpha"  # 注释:name字段的值必须全部为字母,否则抛出后面的异常。
        return value                                  # 注释:返回name字段的值。
  1. 标准数据案例:创建一个字典,里面加入 User 类所有必须字段以及相应的值,然后解包发送给 User 类进行数据校验,校验数据通过以后,返回 user 对象,通过 对象.字段 获取对应的值。
  2. 不符规范报错:当字段值不符合 User 类中对应字段所定义的规范时,会抛出错误。
python
# 标准数据(每个键的值都符合User类中字段的定义)
data = {
    'name': 'Tim',
}
user = User(**data)  # 注释:**data解包data里面的数据。
print(user)          # 输出:name='Tim'
print(user.name)     # 输出:Tim
print(user.dict())   # 输出:{'name': 'Tim'}
python
# 不符规范(name字段在User类的定义必须全部为字母,这里给的Tim123不符合规范)
data = {
    'name': 'Tim123',
}
user = User(**data)  # 报错:name mast be alpha

类型嵌套

Pydantic 还可以使用类型嵌套来校验字段的值。这里我们检查 addr 字段的值是否包含 province 省份、city 城市,代码如下:

python
# 从pydantic中导入BaseModel基础模型类
from pydantic import BaseModel

class Address(BaseModel):
    """Address类继承BaseModel类"""
    province: str  # 注释:province需要一个str类型,必须字段。
    city: str      # 注释:city需要一个str类型,必须字段。

class User(BaseModel):
    """User类继承BaseModel类"""
    name: str      # 注释:name需要一个str类型,必须字段。
    addr: Address  # 注释:addr需要一个Address类型,必须字段。
  1. 标准数据案例:创建一个字典,里面加入 User 类所有必须字段以及相应的值,然后解包发送给 User 类进行数据校验,校验数据通过以后,返回 user 对象,通过 对象.字段 获取对应的值。
  2. 字段缺失错误:当数据中缺少类中的必须字段时,会抛出参数缺少错误。
python
# 标准数据(每个键的值都符合User类中字段的定义)
data = {
    'name': 'Tim',
    'addr': {
        'province': 'SiChuan',
        'city': 'ChengDu'
    }
}
user = User(**data)  # 注释:**data解包data里面的数据。
print(user)          # 输出:name='Tim' addr=Address(province='SiChuan', city='ChengDu')
print(user.name)     # 输出:Tim
print(user.addr)     # 输出:province='SiChuan' city='ChengDu'
print(user.dict())   # 输出:{'name': 'Tim', 'addr': {'province': 'SiChuan', 'city': 'ChengDu'}}
python
# 字段缺失(addr在Address类中定义必须有province、city字段,这里只有province字段)
data = {
    'name': 'Tim',
    'addr': {
        'province': 'SiChuan',
    }
}
user = User(**data)  # 报错:addr字段中缺乏city字段。

校验 JSON

上面提到向服务器发起请求的时候,我们更希望把数据放在请求体中,那么在请求体中的数据肯定是有一个格式的,这个格式就由请求头中 content-type 字段来确定,当该字段的值为 application/json 时,就表明请求体中携带的就是 JSON 格式的数据

单个对象

首先,接着前面的项目,在项目的 son 包中新建一个 son03.py 文件,写入如下代码:

image-20240728233854637

python
# 从fastapi中导入APIRouter接口路由类
from fastapi import APIRouter

son03 = APIRouter()

@son03.post(path='/user')
async def user():
    return {}

接着我们再去到 example05.py 文件中,写入下面的代码:

image-20240728234116245

python
from son.son03 import son03

# 将子路由son03注册全局路由app中
app.include_router(router=son03, tags=["03 请求体数据"])

运行代码后,我们访问本机 5000 端口的 /docs 交互文档,就能看到 /user 的接口文档生成了,具体如下:当中 No parameters 表示当前接口没有任何的路径参数和查询参数

image-20240728234256494

现在假如有人/user 接口发送 POST 请求来获取数据,那么我们就需要对 POST 请求中的请求体数据进行校验,如果校验通过就返回数据,否则就抛出异常。这里就可以使用上面学习的 Pydantic 来校验数据了,修改 son03.py 文件,代码如下:

image-20240804165416117

python
# 从fastapi中导入APIRouter接口路由类
from fastapi import APIRouter
# 从pydantic中导入BaseModel基础模型类
from pydantic import BaseModel
# 从datetime中导入date日期类
from datetime import date
# 从typing中导入List类
from typing import List

son03 = APIRouter()

class User(BaseModel):
    """User类继承BaseModel类"""
    name: str
    age: int = 0
    birth: date = date(2000, 1, 1)
    friends_id: List[int]

@son03.post(path='/user')
# data: User,表示data请求体数据必须符合User类型(就是前面参数类型限制的语法)
async def user(data: User):
    return data

服务器重启后,我们再去访问本机 5000 端口的 /docs 交互文档,然后进行如下测试:

  1. 展开 /user 接口文档,可以看到里面多了一个 Request body 请求体部分,而且是 required 说明必填项,而且还在下方例举了标准的请求体数据
  2. 点击 Try it out 按钮,里面默认填写的是标准的请求体数据,点击 Execute 按钮向服务器发起请求。
  3. 在服务器的响应中我们可以看到,接口文档发起的是 POST 请求,在 Response body 响应体中返回了 Request body 请求体中的数据
  4. 这里我们测试,将接口文档中的 age 字段的值修改为 str 字符串,点击 Execute 按钮向服务器发起请求。
  5. 在服务器的响应中我们可以看到,接口文档发起的是 POST 请求,请求头中 content-type 字段的值为 application/json,表明请求体中携带的就是 JSON 格式的数据。在 Response body 响应体中返回了 msg 错误,提示 age 字段的值应该是 int 类型,而这里输入了 hello 字符类型

::: image-group

<1>

<2>

image-20240804170503026

<4>

<5>

:::

多个对象

Pydantic 不仅可以校验单个对象的数据,还可以校验多个对象的数据,原理就是我们上面学习 Pydantic 讲的类型嵌套。修改 son03.py 文件,代码如下:

image-20240804172353466

python
class UserList(BaseModel):
    """UserList类继承BaseModel类"""
    user_list: List[User]  # 注释:list列表里面必须是User类型对象

@son03.post(path='/user_list')
# data: UserList,表示data请求体数据必须符合UserList类型(就是前面参数类型限制的语法)
async def user(data: UserList):
    return data

服务器重启后,我们再去访问本机 5000 端口的 /docs 交互文档,然后进行如下测试:

  1. 展开 /user_list 接口文档,可以看到标准的请求体数据里面多了一个 user_list 列表将 User 对象的数据进行了包裹
  2. 点击 Try it out 按钮,在标准的请求体数据中的 user_list 列表中再添加一组数据,点击 Execute 按钮向服务器发起请求。
  3. 在服务器的响应中我们可以看到,接口文档发起的是 POST 请求,在 Response body 响应体中返回了 Request body 请求体中的数据

::: image-group

<1>

<2>

<3>

:::

校验 Form

上面提到请求体中的数据肯定是有一个格式的,这个格式就由请求头中 content-type 字段来确定,当该字段的值为 application/json 时,就表明请求体中携带的就是 JSON 格式的数据。如果该字段的值为 application/x-www-form-urlencoded 时,就表明请求体中携带的就是 Form 表单数据

建议

在 OAuth2 规范的一种使用方式(密码流)中,需要将用户名、密码作为表单字段发送,而不是作为 JSON 数据发送。另外,检查一下虚拟环境中是否存在 python-multipart 库,通常该库会随 FastAPI 一同安装,如果没有安装该库,使用 pip install python-multipart==0.0.9 命令进行安装。

发送表单

FastAPI 使用 Form 组件来接收表单数据,具体使用方法看下面的代码:

image-20240804181631935

python
# 从fastapi中导入APIRouter接口路由类、Form表单类
from fastapi import APIRouter, Form

son04 = APIRouter()

@son04.post(path='/register')
# username: str = Form(),表示username是一个str类型的表单数据
async def register(
        username: str = Form(),
        password: str = Form()
):
    return {'username': username, 'password': password}

服务器重启后,我们再去访问本机 5000 端口的 /docs 交互文档,然后进行如下测试:

  1. 展开 /register 接口文档,可以看到在 Request body 请求体右侧是 application/x-www-form-urlencoded,表明该请求体中的数据是 Form 表单,下面的 usernamepassword 就是表单中的字段
  2. 点击 Try it out 按钮,usernamepassword 字段中填入值,点击 Execute 按钮向服务器发起请求。
  3. 在服务器的响应中我们可以看到,接口文档发起的是 POST 请求,在 Response body 响应体中返回了 Form 表单中的数据

::: image-group

<1>

<2>

<3>

:::

内容要求

上面我们写了一个 register 注册函数,但有一个问题就是注册函数中没有检验 usernamepassword 两个表单字段的内容。一般来说,我们进行注册时,都会对用户名和密码的内容进行一定的要求,比如用户名不能超过 16 位,密码不能短于 6 位等。这里我们在 Form 组件中进行验证,代码如下:

image-20240805000643672

python
# 从fastapi中导入APIRouter接口路由类、Form表单类
from fastapi import APIRouter, Form

son04 = APIRouter()

@son04.post(path='/register')
# max_length最大长度,min_length最小长度,regex正则表达式
async def register(
        username: str = Form(min_length=3, max_length=16, regex='^[a-zA-Z]{3,16}$'),
        password: str = Form(min_length=6, max_length=16, regex='^[0-9]{6,16}$')
):
    return {'username': username, 'password': password}

服务器重启后,我们再去访问本机 5000 端口的 /docs 交互文档,然后进行如下测试:

  1. 展开 /register 接口文档,可以看到在 Request body 请求体下面的 usernamepassword 表单字段中,就展示了字段的内容限制规则
  2. 点击 Try it out 按钮,usernamepassword 字段中填入符合内容限制规则的值,点击 Execute 按钮向服务器发起请求。
  3. 在服务器的响应中我们可以看到,接口文档发起的是 POST 请求,在 Response body 响应体中返回了 Form 表单中的数据
  4. 如果说,我们在 username 字段中填入不符合内容限制规则的值,点击 Execute 按钮向服务器发起请求。
  5. 在服务器的响应中我们可以看到,接口文档发起的是 POST 请求,请求头中 content-type 字段的值为 application/x-www-form-urlencoded,表明请求体中携带的是 Form 表单数据。在 Response body 响应体中返回了 msg 错误,提示 username 字段的值必须匹配 ^[a-zA-Z]{3,16}$ 正则,而这里的 username 字段中有数字 123 字符

::: image-group

<1>

<2>

<3>

<4>

image-20240805001620469

:::