FastAPI 入门:用 Python 快速开发现代后端服务

摘要

FastAPI 是近年来 Python Web 开发中非常受欢迎的现代 Web 框架。它基于 Python 类型提示进行参数校验和接口描述,使用 Pydantic 处理数据模型,并运行在 ASGI 生态之上,能够较好地支持异步请求、自动生成 OpenAPI 文档和高效的 API 开发。

本文将从 Flask 与 FastAPI 的开发模型差异开始,介绍 FastAPI 的安装、路由、路径参数、查询参数、请求体、响应模型、依赖注入、错误处理和自动文档。随后通过一个任务管理 API 实战,展示如何使用异步接口、SQLite 数据库和项目分层构建一个更接近生产实践的 FastAPI 服务。

读完本文后,你应该能够:

  • 理解 FastAPI 的核心设计;
  • 区分 WSGI 同步模型与 ASGI 异步模型;
  • 使用类型提示定义 API 参数;
  • 使用 Pydantic 进行请求和响应校验;
  • 使用 FastAPI 自动生成接口文档;
  • 理解依赖注入和路由拆分;
  • 编写同步和异步接口;
  • 组织一个可扩展的 FastAPI 项目。

一、背景与问题

1. 从 Flask 到 FastAPI

Flask 适合学习 Web 基础和快速构建 API,但随着项目发展,开发者通常需要额外解决:

  • 请求参数校验;
  • 响应数据格式;
  • API 文档;
  • 异步请求;
  • 依赖复用;
  • 数据模型转换;
  • 类型检查。

FastAPI 的设计目标,就是把这些常见能力更紧密地整合到框架中:

text 复制代码
Python 类型提示
  -> 参数解析
  -> 数据校验
  -> OpenAPI 文档
  -> 编辑器提示

例如,下面的接口声明同时表达了路径参数的类型和返回类型:

python 复制代码
@app.get("/users/{user_id}")
def get_user(user_id: int) -> UserResponse:
    ...

FastAPI 可以根据这些类型信息判断:

  • user_id 必须是整数;
  • 接口返回结构应该符合 UserResponse;
  • 文档中应该展示什么参数;
  • 参数校验失败时返回什么错误。

2. 现代后端服务的需求

一个现代 Web API 通常需要:

  • 清晰的接口声明;
  • 自动生成文档;
  • 可靠的参数校验;
  • 可复用的依赖;
  • 同步和异步支持;
  • 结构化错误;
  • 数据库连接管理;
  • 测试客户端;
  • 可观测性;
  • 容器化部署。

FastAPI 并不会自动解决所有生产问题,但它提供了比较清晰的基础抽象。

3. 同步和异步的区别

同步代码:

python 复制代码
def get_user():
    result = call_database()
    return result

执行期间,当前线程会等待 call_database 返回。

异步代码:

python 复制代码
async def get_user():
    result = await call_database_async()
    return result

当任务等待 IO 时,事件循环可以处理其他任务。

异步更适合:

  • 大量网络请求;
  • 多个外部 API 调用;
  • WebSocket;
  • 流式响应;
  • 高并发 IO 服务。

异步并不意味着所有代码都必须写成 async。CPU 密集型任务仍然需要进程、线程池或专门的计算服务。

4. FastAPI 的核心组成

可以把 FastAPI 的能力概括为:

text 复制代码
Starlette
  -> Web 层、路由、中间件、ASGI

Pydantic
  -> 数据模型、类型校验、序列化

FastAPI
  -> 参数注入、依赖注入、OpenAPI 文档

它们共同组成一个适合开发 API 的框架。

二、核心概念

1. ASGI

ASGI 是 Python 异步 Web 服务接口规范,支持:

  • 普通 HTTP 请求;
  • 异步处理;
  • WebSocket;
  • 长连接;
  • 流式响应。

传统 WSGI 通常以同步调用为核心:

text 复制代码
服务器
  -> 调用应用
  -> 应用处理请求
  -> 返回响应

ASGI 可以支持异步应用:

text 复制代码
事件循环
  -> 接收连接
  -> 调度协程
  -> 在等待 IO 时切换任务
  -> 返回响应

FastAPI 可以运行在 Uvicorn、Hypercorn 等 ASGI 服务器上。

2. 路由

FastAPI 使用装饰器声明路由:

python 复制代码
from fastapi import FastAPI


app = FastAPI()


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

常见路由装饰器:

python 复制代码
@app.get("/items")
@app.post("/items")
@app.put("/items/{item_id}")
@app.patch("/items/{item_id}")
@app.delete("/items/{item_id}")

路径和方法清晰表达了资源操作。

3. 路径参数

python 复制代码
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return {
        "user_id": user_id
    }

访问:

text 复制代码
GET /users/10

user_id 会被解析为整数。如果访问:

text 复制代码
GET /users/abc

FastAPI 会自动返回参数校验错误。

4. 查询参数

python 复制代码
@app.get("/items")
async def list_items(
    page: int = 1,
    page_size: int = 20,
    keyword: str | None = None,
):
    return {
        "page": page,
        "page_size": page_size,
        "keyword": keyword,
    }

查询:

text 复制代码
GET /items?page=2&page_size=10&keyword=python

还可以通过 Query 定义约束:

python 复制代码
from fastapi import Query


@app.get("/items")
async def list_items(
    page: int = Query(
        default=1,
        ge=1,
    ),
    page_size: int = Query(
        default=20,
        ge=1,
        le=100,
    ),
):
    return {
        "page": page,
        "page_size": page_size,
    }

5. 请求体模型

使用 Pydantic 定义请求模型:

python 复制代码
from pydantic import BaseModel, Field


class TodoCreate(BaseModel):
    title: str = Field(
        min_length=1,
        max_length=100,
    )
    description: str = Field(
        default="",
        max_length=1000,
    )

接口声明:

python 复制代码
@app.post("/todos")
async def create_todo(
    payload: TodoCreate,
):
    return {
        "title": payload.title,
        "description": payload.description,
    }

FastAPI 会自动:

  • 读取 JSON;
  • 转换字段类型;
  • 检查必填字段;
  • 检查长度;
  • 返回校验错误;
  • 将模型展示到 OpenAPI 文档。

6. 响应模型

python 复制代码
class TodoResponse(BaseModel):
    id: int
    title: str
    description: str
    completed: bool


@app.get(
    "/todos/{todo_id}",
    response_model=TodoResponse,
)
async def get_todo(todo_id: int):
    return {
        "id": todo_id,
        "title": "学习 FastAPI",
        "description": "",
        "completed": False,
        "internal_field": "不会返回",
    }

response_model 可以过滤未声明的字段,避免内部数据意外泄露。

7. 依赖注入

FastAPI 使用 Depends 声明依赖:

python 复制代码
from fastapi import Depends


async def get_current_user():
    return {
        "id": 1,
        "name": "Alice",
    }


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

依赖可以用于:

  • 数据库会话;
  • 当前用户;
  • 权限检查;
  • 分页参数;
  • 配置对象;
  • 外部客户端;
  • 请求上下文。

8. 自动文档

启动 FastAPI 后,默认可以访问:

text 复制代码
/docs
/redoc
/openapi.json

OpenAPI 文档会根据:

  • 路由;
  • 方法;
  • 参数类型;
  • Pydantic 模型;
  • 响应模型;
  • 描述信息;

自动生成。

9. HTTPException

业务中可以抛出 HTTPException:

python 复制代码
from fastapi import HTTPException


@app.get("/todos/{todo_id}")
async def get_todo(todo_id: int):
    todo = find_todo(todo_id)

    if todo is None:
        raise HTTPException(
            status_code=404,
            detail="待办事项不存在",
        )

    return todo

对于大型项目,可以进一步统一业务异常和错误响应格式。

三、工作原理

1. FastAPI 请求处理流程

#mermaid-svg-fkZxk5shCIh37wTU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fkZxk5shCIh37wTU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fkZxk5shCIh37wTU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fkZxk5shCIh37wTU .error-icon{fill:#552222;}#mermaid-svg-fkZxk5shCIh37wTU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fkZxk5shCIh37wTU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fkZxk5shCIh37wTU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fkZxk5shCIh37wTU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fkZxk5shCIh37wTU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fkZxk5shCIh37wTU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fkZxk5shCIh37wTU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fkZxk5shCIh37wTU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fkZxk5shCIh37wTU .marker.cross{stroke:#333333;}#mermaid-svg-fkZxk5shCIh37wTU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fkZxk5shCIh37wTU p{margin:0;}#mermaid-svg-fkZxk5shCIh37wTU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fkZxk5shCIh37wTU .cluster-label text{fill:#333;}#mermaid-svg-fkZxk5shCIh37wTU .cluster-label span{color:#333;}#mermaid-svg-fkZxk5shCIh37wTU .cluster-label span p{background-color:transparent;}#mermaid-svg-fkZxk5shCIh37wTU .label text,#mermaid-svg-fkZxk5shCIh37wTU span{fill:#333;color:#333;}#mermaid-svg-fkZxk5shCIh37wTU .node rect,#mermaid-svg-fkZxk5shCIh37wTU .node circle,#mermaid-svg-fkZxk5shCIh37wTU .node ellipse,#mermaid-svg-fkZxk5shCIh37wTU .node polygon,#mermaid-svg-fkZxk5shCIh37wTU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fkZxk5shCIh37wTU .rough-node .label text,#mermaid-svg-fkZxk5shCIh37wTU .node .label text,#mermaid-svg-fkZxk5shCIh37wTU .image-shape .label,#mermaid-svg-fkZxk5shCIh37wTU .icon-shape .label{text-anchor:middle;}#mermaid-svg-fkZxk5shCIh37wTU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fkZxk5shCIh37wTU .rough-node .label,#mermaid-svg-fkZxk5shCIh37wTU .node .label,#mermaid-svg-fkZxk5shCIh37wTU .image-shape .label,#mermaid-svg-fkZxk5shCIh37wTU .icon-shape .label{text-align:center;}#mermaid-svg-fkZxk5shCIh37wTU .node.clickable{cursor:pointer;}#mermaid-svg-fkZxk5shCIh37wTU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fkZxk5shCIh37wTU .arrowheadPath{fill:#333333;}#mermaid-svg-fkZxk5shCIh37wTU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fkZxk5shCIh37wTU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fkZxk5shCIh37wTU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fkZxk5shCIh37wTU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fkZxk5shCIh37wTU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fkZxk5shCIh37wTU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fkZxk5shCIh37wTU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fkZxk5shCIh37wTU .cluster text{fill:#333;}#mermaid-svg-fkZxk5shCIh37wTU .cluster span{color:#333;}#mermaid-svg-fkZxk5shCIh37wTU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fkZxk5shCIh37wTU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fkZxk5shCIh37wTU rect.text{fill:none;stroke-width:0;}#mermaid-svg-fkZxk5shCIh37wTU .icon-shape,#mermaid-svg-fkZxk5shCIh37wTU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fkZxk5shCIh37wTU .icon-shape p,#mermaid-svg-fkZxk5shCIh37wTU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fkZxk5shCIh37wTU .icon-shape .label rect,#mermaid-svg-fkZxk5shCIh37wTU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fkZxk5shCIh37wTU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fkZxk5shCIh37wTU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fkZxk5shCIh37wTU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTTP 请求
ASGI 服务器
FastAPI 应用
路由匹配
解析路径和查询参数
解析请求体
执行依赖
调用处理函数
校验响应模型
序列化响应
返回客户端

参数校验和依赖解析发生在处理函数执行前。响应模型处理发生在业务函数返回之后。

2. 同步函数和异步函数

FastAPI 可以同时定义同步和异步处理函数:

python 复制代码
@app.get("/sync")
def sync_endpoint():
    return {
        "mode": "sync"
    }


@app.get("/async")
async def async_endpoint():
    return {
        "mode": "async"
    }

如果处理函数是普通 def,框架通常会将其放到线程池中执行,避免阻塞事件循环。如果处理函数是 async def,则在事件循环中运行。

注意:不要在 async def 中直接执行长时间阻塞的同步代码:

python 复制代码
@app.get("/bad")
async def bad_endpoint():
    result = blocking_database_call()
    return result

更好的选择:

  • 使用异步数据库驱动;
  • 将同步函数放入线程池;
  • 将耗时任务放到后台队列;
  • 使用专门的计算服务。

3. 依赖解析

依赖注入可以形成依赖树:
#mermaid-svg-K00UoDtdCiqCsep7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-K00UoDtdCiqCsep7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-K00UoDtdCiqCsep7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-K00UoDtdCiqCsep7 .error-icon{fill:#552222;}#mermaid-svg-K00UoDtdCiqCsep7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-K00UoDtdCiqCsep7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-K00UoDtdCiqCsep7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-K00UoDtdCiqCsep7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-K00UoDtdCiqCsep7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-K00UoDtdCiqCsep7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-K00UoDtdCiqCsep7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-K00UoDtdCiqCsep7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-K00UoDtdCiqCsep7 .marker.cross{stroke:#333333;}#mermaid-svg-K00UoDtdCiqCsep7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-K00UoDtdCiqCsep7 p{margin:0;}#mermaid-svg-K00UoDtdCiqCsep7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-K00UoDtdCiqCsep7 .cluster-label text{fill:#333;}#mermaid-svg-K00UoDtdCiqCsep7 .cluster-label span{color:#333;}#mermaid-svg-K00UoDtdCiqCsep7 .cluster-label span p{background-color:transparent;}#mermaid-svg-K00UoDtdCiqCsep7 .label text,#mermaid-svg-K00UoDtdCiqCsep7 span{fill:#333;color:#333;}#mermaid-svg-K00UoDtdCiqCsep7 .node rect,#mermaid-svg-K00UoDtdCiqCsep7 .node circle,#mermaid-svg-K00UoDtdCiqCsep7 .node ellipse,#mermaid-svg-K00UoDtdCiqCsep7 .node polygon,#mermaid-svg-K00UoDtdCiqCsep7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-K00UoDtdCiqCsep7 .rough-node .label text,#mermaid-svg-K00UoDtdCiqCsep7 .node .label text,#mermaid-svg-K00UoDtdCiqCsep7 .image-shape .label,#mermaid-svg-K00UoDtdCiqCsep7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-K00UoDtdCiqCsep7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-K00UoDtdCiqCsep7 .rough-node .label,#mermaid-svg-K00UoDtdCiqCsep7 .node .label,#mermaid-svg-K00UoDtdCiqCsep7 .image-shape .label,#mermaid-svg-K00UoDtdCiqCsep7 .icon-shape .label{text-align:center;}#mermaid-svg-K00UoDtdCiqCsep7 .node.clickable{cursor:pointer;}#mermaid-svg-K00UoDtdCiqCsep7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-K00UoDtdCiqCsep7 .arrowheadPath{fill:#333333;}#mermaid-svg-K00UoDtdCiqCsep7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-K00UoDtdCiqCsep7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-K00UoDtdCiqCsep7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-K00UoDtdCiqCsep7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-K00UoDtdCiqCsep7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-K00UoDtdCiqCsep7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-K00UoDtdCiqCsep7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-K00UoDtdCiqCsep7 .cluster text{fill:#333;}#mermaid-svg-K00UoDtdCiqCsep7 .cluster span{color:#333;}#mermaid-svg-K00UoDtdCiqCsep7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-K00UoDtdCiqCsep7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-K00UoDtdCiqCsep7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-K00UoDtdCiqCsep7 .icon-shape,#mermaid-svg-K00UoDtdCiqCsep7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-K00UoDtdCiqCsep7 .icon-shape p,#mermaid-svg-K00UoDtdCiqCsep7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-K00UoDtdCiqCsep7 .icon-shape .label rect,#mermaid-svg-K00UoDtdCiqCsep7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-K00UoDtdCiqCsep7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-K00UoDtdCiqCsep7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-K00UoDtdCiqCsep7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 路由处理函数
当前用户依赖
数据库会话依赖
认证 Token 依赖
数据库连接池

FastAPI 会按照依赖关系解析参数,处理共享依赖,并在请求结束后清理资源。

4. Pydantic 的作用

Pydantic 模型主要用于:

  • 数据解析;
  • 类型转换;
  • 参数校验;
  • 序列化;
  • OpenAPI Schema 生成。

例如:

python 复制代码
class UserCreate(BaseModel):
    username: str = Field(
        min_length=3,
        max_length=50,
    )
    age: int = Field(
        ge=0,
        le=150,
    )
    email: str

传入不符合约束的数据时,接口会返回结构化错误。

Pydantic 模型适合表达 API 边界,但不要把所有复杂业务规则都塞进字段校验中。跨字段、依赖数据库状态的规则仍然应该放在服务层。

5. 生命周期事件

应用启动和关闭时可能需要初始化和释放资源:

  • 创建数据库连接池;
  • 初始化 HTTP 客户端;
  • 加载配置;
  • 关闭连接;
  • 释放线程池;
  • 保存统计信息。

可以使用 lifespan:

python 复制代码
from contextlib import asynccontextmanager
from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.http_client = create_http_client()

    yield

    await app.state.http_client.aclose()


app = FastAPI(
    lifespan=lifespan
)

资源应该在应用生命周期内复用,不要每个请求都重新创建连接池。

6. 中间件

中间件可以在请求前后执行通用逻辑:

python 复制代码
import time
from starlette.middleware.base import (
    BaseHTTPMiddleware,
)


class RequestTimingMiddleware(
    BaseHTTPMiddleware
):
    async def dispatch(
        self,
        request,
        call_next,
    ):
        started = time.perf_counter()

        response = await call_next(request)

        elapsed = (
            time.perf_counter()
            - started
        ) * 1000

        response.headers[
            "X-Process-Time-Ms"
        ] = f"{elapsed:.2f}"

        return response

中间件适合处理:

  • 请求 ID;
  • 日志;
  • CORS;
  • 认证前置检查;
  • 指标;
  • 全局响应头;
  • 统一异常记录。

四、实战示例

1. 创建项目

创建虚拟环境:

bash 复制代码
mkdir fastapi-todo-api
cd fastapi-todo-api
python -m venv .venv

激活环境后安装:

bash 复制代码
python -m pip install fastapi uvicorn

为了使用 SQLite 异步访问,再安装:

bash 复制代码
python -m pip install aiosqlite

保存依赖:

bash 复制代码
python -m pip freeze > requirements.txt

2. 项目结构

text 复制代码
fastapi-todo-api/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── config.py
│   ├── database.py
│   ├── dependencies.py
│   └── todos/
│       ├── __init__.py
│       ├── router.py
│       ├── schemas.py
│       ├── service.py
│       └── repository.py
├── tests/
│   └── test_todos.py
└── requirements.txt

3. 创建应用

app/main.py:

python 复制代码
from fastapi import FastAPI

from app.todos.router import router as todo_router


app = FastAPI(
    title="Todo API",
    version="1.0.0",
    description="一个 FastAPI 待办事项服务",
)

app.include_router(
    todo_router,
    prefix="/api/todos",
    tags=["todos"],
)


@app.get("/health")
async def health():
    return {
        "status": "ok"
    }

启动:

bash 复制代码
uvicorn app.main:app --reload

访问:

text 复制代码
http://127.0.0.1:8000/docs

4. 定义 Pydantic 模型

app/todos/schemas.py:

python 复制代码
from datetime import datetime

from pydantic import (
    BaseModel,
    ConfigDict,
    Field,
)


class TodoCreate(BaseModel):
    title: str = Field(
        min_length=1,
        max_length=100,
    )
    description: str = Field(
        default="",
        max_length=1000,
    )


class TodoUpdate(BaseModel):
    title: str | None = Field(
        default=None,
        min_length=1,
        max_length=100,
    )
    description: str | None = Field(
        default=None,
        max_length=1000,
    )
    completed: bool | None = None


class TodoResponse(BaseModel):
    model_config = ConfigDict(
        from_attributes=True
    )

    id: int
    title: str
    description: str
    completed: bool
    created_at: datetime
    updated_at: datetime


class TodoListResponse(BaseModel):
    items: list[TodoResponse]
    total: int
    page: int
    page_size: int

请求模型和响应模型分开,可以避免客户端提交或看到不应该暴露的字段。

5. 初始化数据库

app/database.py:

python 复制代码
from pathlib import Path

import aiosqlite


DATABASE_PATH = Path("todo.db")


async def init_db():
    async with aiosqlite.connect(
        DATABASE_PATH
    ) as db:
        await db.execute(
            """
            CREATE TABLE IF NOT EXISTS todos (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL,
                description TEXT NOT NULL,
                completed INTEGER NOT NULL DEFAULT 0,
                created_at TEXT NOT NULL,
                updated_at TEXT NOT NULL
            )
            """
        )
        await db.commit()

在应用启动时初始化:

python 复制代码
from contextlib import asynccontextmanager

from app.database import init_db


@asynccontextmanager
async def lifespan(app: FastAPI):
    await init_db()
    yield

将 lifespan 传入应用:

python 复制代码
app = FastAPI(
    title="Todo API",
    lifespan=lifespan,
)

6. 实现数据访问层

app/todos/repository.py:

python 复制代码
from datetime import datetime, timezone

import aiosqlite

from app.database import DATABASE_PATH


def now_utc():
    return datetime.now(
        timezone.utc
    )


class TodoRepository:
    async def create(
        self,
        title: str,
        description: str,
    ) -> dict:
        timestamp = now_utc().isoformat()

        async with aiosqlite.connect(
            DATABASE_PATH
        ) as db:
            db.row_factory = aiosqlite.Row

            cursor = await db.execute(
                """
                INSERT INTO todos (
                    title,
                    description,
                    completed,
                    created_at,
                    updated_at
                ) VALUES (?, ?, ?, ?, ?)
                """,
                (
                    title,
                    description,
                    0,
                    timestamp,
                    timestamp,
                ),
            )

            await db.commit()

            return await self.get_by_id(
                cursor.lastrowid
            )

    async def get_by_id(
        self,
        todo_id: int,
    ) -> dict | None:
        async with aiosqlite.connect(
            DATABASE_PATH
        ) as db:
            db.row_factory = aiosqlite.Row

            cursor = await db.execute(
                """
                SELECT id, title, description,
                       completed, created_at,
                       updated_at
                FROM todos
                WHERE id = ?
                """,
                (todo_id,),
            )

            row = await cursor.fetchone()

            if row is None:
                return None

            return dict(row)

    async def list(
        self,
        offset: int,
        limit: int,
        completed: bool | None = None,
    ) -> tuple[list[dict], int]:
        async with aiosqlite.connect(
            DATABASE_PATH
        ) as db:
            db.row_factory = aiosqlite.Row

            if completed is None:
                where = ""
                params = ()
            else:
                where = "WHERE completed = ?"
                params = (int(completed),)

            count_cursor = await db.execute(
                f"""
                SELECT COUNT(*)
                FROM todos
                {where}
                """,
                params,
            )
            total = (
                await count_cursor.fetchone()
            )[0]

            cursor = await db.execute(
                f"""
                SELECT id, title, description,
                       completed, created_at,
                       updated_at
                FROM todos
                {where}
                ORDER BY id DESC
                LIMIT ? OFFSET ?
                """,
                params + (limit, offset),
            )

            rows = await cursor.fetchall()

            return [
                dict(row)
                for row in rows
            ], total

where 条件是程序内部固定字符串,查询参数仍然使用绑定参数。复杂项目应该使用更成熟的数据库会话和连接池管理方案。

7. 实现更新和删除

python 复制代码
    async def update(
        self,
        todo_id: int,
        values: dict,
    ) -> dict | None:
        if not values:
            return await self.get_by_id(todo_id)

        values["updated_at"] = now_utc().isoformat()

        fields = []
        params = []

        allowed_fields = {
            "title",
            "description",
            "completed",
            "updated_at",
        }

        for field, value in values.items():
            if field not in allowed_fields:
                continue

            fields.append(f"{field} = ?")
            params.append(value)

        params.append(todo_id)

        async with aiosqlite.connect(
            DATABASE_PATH
        ) as db:
            await db.execute(
                f"""
                UPDATE todos
                SET {", ".join(fields)}
                WHERE id = ?
                """,
                tuple(params),
            )
            await db.commit()

        return await self.get_by_id(todo_id)

    async def delete(
        self,
        todo_id: int,
    ) -> bool:
        async with aiosqlite.connect(
            DATABASE_PATH
        ) as db:
            cursor = await db.execute(
                """
                DELETE FROM todos
                WHERE id = ?
                """,
                (todo_id,),
            )
            await db.commit()
            return cursor.rowcount > 0

动态字段名不能使用 SQL 参数绑定,因此必须通过 allowed_fields 白名单限制。不能直接把客户端提交的字段名拼接到 SQL 中。

8. 实现服务层

app/todos/service.py:

python 复制代码
from fastapi import HTTPException

from app.todos.repository import TodoRepository


class TodoService:
    def __init__(
        self,
        repository: TodoRepository,
    ):
        self.repository = repository

    async def create(
        self,
        title: str,
        description: str,
    ):
        return await self.repository.create(
            title=title.strip(),
            description=description.strip(),
        )

    async def get(self, todo_id: int):
        todo = await self.repository.get_by_id(
            todo_id
        )

        if todo is None:
            raise HTTPException(
                status_code=404,
                detail="待办事项不存在",
            )

        return todo

    async def update(
        self,
        todo_id: int,
        values: dict,
    ):
        todo = await self.repository.get_by_id(
            todo_id
        )

        if todo is None:
            raise HTTPException(
                status_code=404,
                detail="待办事项不存在",
            )

        return await self.repository.update(
            todo_id,
            values,
        )

    async def delete(self, todo_id: int):
        deleted = await self.repository.delete(
            todo_id
        )

        if not deleted:
            raise HTTPException(
                status_code=404,
                detail="待办事项不存在",
            )

服务层负责业务规则和资源不存在处理,路由层只负责参数接收和响应转换。

9. 依赖注入数据库服务

app/dependencies.py:

python 复制代码
from typing import Annotated

from fastapi import Depends

from app.todos.repository import TodoRepository
from app.todos.service import TodoService


def get_todo_repository():
    return TodoRepository()


def get_todo_service(
    repository: Annotated[
        TodoRepository,
        Depends(get_todo_repository),
    ],
):
    return TodoService(repository)

10. 定义路由

app/todos/router.py:

python 复制代码
from typing import Annotated

from fastapi import (
    APIRouter,
    Depends,
    Query,
    Response,
    status,
)

from app.dependencies import (
    get_todo_service,
)
from app.todos.schemas import (
    TodoCreate,
    TodoListResponse,
    TodoResponse,
    TodoUpdate,
)
from app.todos.service import TodoService


router = APIRouter()


@router.post(
    "",
    response_model=TodoResponse,
    status_code=status.HTTP_201_CREATED,
)
async def create_todo(
    payload: TodoCreate,
    service: Annotated[
        TodoService,
        Depends(get_todo_service),
    ],
):
    return await service.create(
        title=payload.title,
        description=payload.description,
    )


@router.get(
    "",
    response_model=TodoListResponse,
)
async def list_todos(
    service: Annotated[
        TodoService,
        Depends(get_todo_service),
    ],
    page: int = Query(
        default=1,
        ge=1,
    ),
    page_size: int = Query(
        default=20,
        ge=1,
        le=100,
    ),
    completed: bool | None = None,
):
    offset = (page - 1) * page_size

    items, total = await service.repository.list(
        offset=offset,
        limit=page_size,
        completed=completed,
    )

    return {
        "items": items,
        "total": total,
        "page": page,
        "page_size": page_size,
    }


@router.get(
    "/{todo_id}",
    response_model=TodoResponse,
)
async def get_todo(
    todo_id: int,
    service: Annotated[
        TodoService,
        Depends(get_todo_service),
    ],
):
    return await service.get(todo_id)


@router.patch(
    "/{todo_id}",
    response_model=TodoResponse,
)
async def update_todo(
    todo_id: int,
    payload: TodoUpdate,
    service: Annotated[
        TodoService,
        Depends(get_todo_service),
    ],
):
    values = payload.model_dump(
        exclude_unset=True
    )

    return await service.update(
        todo_id,
        values,
    )


@router.delete(
    "/{todo_id}",
    status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_todo(
    todo_id: int,
    service: Annotated[
        TodoService,
        Depends(get_todo_service),
    ],
):
    await service.delete(todo_id)
    return Response(status_code=204)

这里的 list_todos 直接访问了 service.repository,演示上可以运行,但更推荐在服务层增加 list 方法,避免路由层穿透服务对象内部结构。

改进:

python 复制代码
async def list(
    self,
    page: int,
    page_size: int,
    completed: bool | None,
):
    offset = (page - 1) * page_size

    items, total = await self.repository.list(
        offset=offset,
        limit=page_size,
        completed=completed,
    )

    return {
        "items": items,
        "total": total,
        "page": page,
        "page_size": page_size,
    }

路由中改为:

python 复制代码
return await service.list(
    page=page,
    page_size=page_size,
    completed=completed,
)

11. 测试接口

安装测试依赖:

bash 复制代码
python -m pip install pytest httpx

tests/test_todos.py:

python 复制代码
from fastapi.testclient import TestClient

from app.main import app


client = TestClient(app)


def test_health():
    response = client.get("/health")

    assert response.status_code == 200
    assert response.json()["status"] == "ok"


def test_create_todo():
    response = client.post(
        "/api/todos",
        json={
            "title": "学习 FastAPI",
            "description": "完成第一个异步 API",
        },
    )

    assert response.status_code == 201

    body = response.json()
    assert body["title"] == "学习 FastAPI"
    assert body["completed"] is False


def test_invalid_todo():
    response = client.post(
        "/api/todos",
        json={
            "title": "",
        },
    )

    assert response.status_code == 422

测试时不应该使用生产数据库。可以通过依赖覆盖替换数据库仓库:

python 复制代码
app.dependency_overrides[
    get_todo_repository
] = get_test_repository

这样测试可以使用临时数据库、内存数据库或 Mock 对象。

五、常见问题与实践建议

1. FastAPI 一定比 Flask 快吗

不能简单地这样比较。性能取决于:

  • 请求类型;
  • 同步或异步 IO;
  • 数据库驱动;
  • 序列化方式;
  • 中间件;
  • 服务器配置;
  • worker 数量;
  • 业务代码;
  • 下游服务。

FastAPI 在类型校验、异步接口和自动文档方面体验很好,但如果接口主要被数据库慢查询限制,换框架未必明显提升性能。

2. async def 中可以调用同步库吗

可以调用,但会阻塞事件循环。例如:

python 复制代码
@app.get("/bad")
async def bad():
    result = requests.get(
        "https://example.com"
    )
    return result.json()

requests 是同步 HTTP 客户端,在 async def 中直接调用会阻塞当前事件循环。

改进方式:

  • 使用 httpx.AsyncClient;
  • 使用异步数据库驱动;
  • 把同步调用放到线程池;
  • 将任务交给后台队列。

3. async def 是否适合 CPU 密集计算

不适合直接在事件循环中执行长时间 CPU 计算:

python 复制代码
@app.get("/bad")
async def bad():
    result = calculate_large_report()
    return result

这会阻塞其他协程。可以:

  • 使用进程池;
  • 使用任务队列;
  • 拆分计算;
  • 使用专门的数据处理服务。

4. 422 和 400 应该如何区分

FastAPI 对请求参数和模型校验失败通常返回 422。它表示请求格式或数据结构不符合接口要求。

400 更适合:

  • 业务层参数不合法;
  • 请求无法被业务处理;
  • 自定义协议错误。

项目中可以统一错误响应结构,但不要为了追求一个状态码而丢失错误语义。

5. response_model 为什么重要

如果直接返回数据库字典:

python 复制代码
return user_dict

可能无意中返回:

  • 密码哈希;
  • 内部权限;
  • 删除标记;
  • 数据库字段;
  • 内部调试信息。

response_model 可以明确允许返回哪些字段:

python 复制代码
class UserResponse(BaseModel):
    id: int
    username: str

任何 API 都应该明确响应边界。

6. Pydantic 模型和 ORM 模型是否应该共用

不建议无条件共用。两者职责不同:

text 复制代码
ORM 模型:
  描述数据库表和持久化行为

Pydantic 模型:
  描述 API 输入输出和校验

可以通过 from_attributes 或转换函数连接两者,但不要把数据库内部结构直接作为公共 API 契约。

7. FastAPI 依赖注入会不会变复杂

依赖注入在依赖较少时非常清晰,但项目变大后需要控制层级。建议:

  • 每个依赖只负责一件事;
  • 依赖命名清楚;
  • 避免隐式修改全局状态;
  • 数据库会话在请求范围内管理;
  • 测试时支持 override;
  • 高风险依赖记录日志。

8. 如何管理配置

使用环境变量和配置模型:

python 复制代码
from pydantic_settings import (
    BaseSettings,
)


class Settings(BaseSettings):
    app_name: str = "Todo API"
    database_url: str = "sqlite:///todo.db"
    debug: bool = False
    request_timeout: float = 5.0

    class Config:
        env_file = ".env"


settings = Settings()

安装配置依赖:

bash 复制代码
python -m pip install pydantic-settings

不要把数据库密码、JWT 密钥和第三方 Token 写进代码。

9. FastAPI 如何处理认证

可以使用 OAuth2、JWT、API Key 等方式。简单的依赖示例:

python 复制代码
from fastapi import Depends, HTTPException
from fastapi.security import (
    HTTPBearer,
)


security = HTTPBearer()


async def get_current_user(
    credentials=Depends(security),
):
    token = credentials.credentials
    user = verify_token(token)

    if user is None:
        raise HTTPException(
            status_code=401,
            detail="无效的认证凭证",
        )

    return user

路由中使用:

python 复制代码
@app.get("/profile")
async def profile(
    user=Depends(get_current_user),
):
    return user

认证和授权仍然要分开,获取当前用户后,还要检查用户是否有访问具体资源的权限。

10. 如何处理后台任务

FastAPI 提供简单的 BackgroundTasks:

python 复制代码
from fastapi import BackgroundTasks


def write_audit_log(user_id: int):
    ...


@app.post("/events")
async def create_event(
    background_tasks: BackgroundTasks,
):
    background_tasks.add_task(
        write_audit_log,
        1,
    )

    return {
        "status": "accepted"
    }

这种方式适合短小、非关键的后台动作。对于长时间任务、重试要求高的任务和跨实例任务,应使用 Celery、RQ、消息队列或持久化任务系统。

11. 如何部署多个 worker

开发环境:

bash 复制代码
uvicorn app.main:app --reload

生产环境可以:

bash 复制代码
uvicorn app.main:app \
  --host 0.0.0.0 \
  --port 8000 \
  --workers 4

或使用 Gunicorn 管理 Uvicorn worker:

bash 复制代码
gunicorn app.main:app \
  -k uvicorn.workers.UvicornWorker \
  --workers 4 \
  --bind 0.0.0.0:8000

worker 数量要结合 CPU 和内存压测,不能盲目增加。

12. 异步数据库是否一定更好

异步数据库可以减少等待时阻塞事件循环,但也会增加:

  • 异步编程复杂度;
  • 连接池管理;
  • 调试成本;
  • 依赖兼容性;
  • 事务边界理解难度。

如果服务流量不高、业务简单,成熟的同步数据库方案也可以满足需求。关键是让数据库访问模型与服务运行模型匹配。

六、进阶思考

1. Flask 与 FastAPI 的选择

维度 Flask FastAPI
学习 HTTP 基础 直观 也较清晰
类型提示整合 需要额外组织 原生体验好
自动文档 需要扩展 内置支持
异步支持 可通过扩展和服务器实现 更自然
生态成熟度 历史久、生态广 现代 API 体验好
自由度 很高 约定更多
适合场景 轻量服务、传统同步应用 API、异步 IO、数据服务

两者不是绝对替代关系。选择框架时应看团队经验、业务模型、依赖生态和部署环境。

2. ASGI 下的并发模型

一个事件循环可以管理多个协程:

text 复制代码
请求 A 等待数据库
  -> 切换处理请求 B

请求 B 等待 HTTP 服务
  -> 切换处理请求 C

请求 C 计算完成
  -> 返回响应

这要求所有等待型操作都具有异步特性。如果其中一个协程执行阻塞代码,整个事件循环都可能受影响。

因此异步项目的性能关键是:

  • 识别阻塞调用;
  • 选择正确的异步客户端;
  • 设置连接和读取超时;
  • 控制协程数量;
  • 限制数据库连接;
  • 对 CPU 任务隔离。

3. 数据库会话和连接池

生产服务不应为每个 SQL 临时创建底层连接。应该使用连接池:

text 复制代码
应用启动
  -> 创建数据库连接池

请求到达
  -> 从连接池获取连接
  -> 执行事务
  -> 归还连接

应用关闭
  -> 关闭连接池

连接池参数需要考虑:

  • 最大连接数;
  • 最小连接数;
  • 获取连接超时;
  • 空闲超时;
  • 连接回收;
  • 数据库最大连接;
  • worker 数量。

多个 worker 进程会分别创建连接池,所以总连接数需要按进程计算。

4. 统一异常模型

可以定义业务异常:

python 复制代码
class AppError(Exception):
    def __init__(
        self,
        code: str,
        message: str,
        status_code: int = 400,
    ):
        self.code = code
        self.message = message
        self.status_code = status_code

统一异常处理:

python 复制代码
from fastapi import Request
from fastapi.responses import JSONResponse


@app.exception_handler(AppError)
async def handle_app_error(
    request: Request,
    exc: AppError,
):
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "code": exc.code,
            "message": exc.message,
        },
    )

统一格式有利于前端处理、日志统计和接口文档。

5. 中间件和依赖如何选择

适合中间件:

  • 所有请求都需要的逻辑;
  • 请求 ID;
  • 统一耗时;
  • CORS;
  • 全局日志;
  • 全局异常记录。

适合依赖:

  • 当前用户;
  • 当前数据库会话;
  • 某个路由组的权限;
  • 分页和筛选参数;
  • 可复用的业务前置检查。

不要把所有逻辑都写进中间件,否则业务边界会变得不清晰。

6. API 版本和兼容性

当 API 对外提供服务时,应考虑版本:

text 复制代码
/api/v1/todos
/api/v2/todos

接口升级时要保持:

  • 旧客户端短期可用;
  • 字段新增向后兼容;
  • 删除字段有迁移周期;
  • 错误码有文档;
  • OpenAPI 文档同步;
  • 监控区分不同版本。

7. 流式响应

FastAPI 适合处理流式响应:

python 复制代码
from fastapi.responses import (
    StreamingResponse,
)


async def event_stream():
    for index in range(3):
        yield f"data: chunk-{index}\n\n"


@app.get("/stream")
async def stream():
    return StreamingResponse(
        event_stream(),
        media_type="text/event-stream",
    )

流式接口需要考虑:

  • 客户端断开;
  • 连接超时;
  • 心跳;
  • 资源释放;
  • 代理缓冲;
  • 并发连接;
  • 取消任务。

8. 可观测性

建议为 FastAPI 服务记录:

text 复制代码
请求量
状态码分布
P50、P95、P99
请求体大小
响应体大小
数据库耗时
外部服务耗时
连接池使用
事件循环阻塞
异常类型
用户和租户维度

每个请求都应带 request_id 或 trace_id:

python 复制代码
import uuid


@app.middleware("http")
async def add_request_id(
    request,
    call_next,
):
    request_id = request.headers.get(
        "X-Request-Id"
    ) or str(uuid.uuid4())

    response = await call_next(request)
    response.headers[
        "X-Request-Id"
    ] = request_id

    return response

9. 容器化部署

Dockerfile 示例:

dockerfile 复制代码
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app ./app

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

生产环境还需要考虑:

  • 非 root 用户;
  • 健康检查;
  • 资源限制;
  • 日志输出到标准输出;
  • 配置通过环境变量注入;
  • 优雅关闭;
  • 多副本;
  • 反向代理;
  • 数据库迁移。

结论

FastAPI 将 Python 类型提示、Pydantic 数据模型、ASGI 异步能力、依赖注入和 OpenAPI 文档结合起来,为构建现代 Web API 提供了比较完整的开发体验。

本文通过待办事项服务介绍了:

  • FastAPI 应用和路由;
  • 路径参数和查询参数;
  • Pydantic 请求与响应模型;
  • 依赖注入;
  • 自动 API 文档;
  • SQLite 异步访问;
  • 项目分层;
  • 接口测试;
  • 同步与异步代码;
  • 部署和可观测性。

学习 FastAPI 时,建议按照以下路径继续:

text 复制代码
路由和参数
  -> Pydantic 模型
  -> 依赖注入
  -> 数据库会话
  -> 认证授权
  -> 异步客户端
  -> 后台任务
  -> 流式响应
  -> 容器化和监控

FastAPI 的优势不只在于代码简洁,更在于它把 API 的输入、输出和依赖关系显式表达出来。当项目逐步变大时,这些显式约束能够减少参数错误、文档滞后和接口边界混乱。

下一篇可以继续学习《FastAPI 的依赖注入、参数校验与接口文档》,深入分析 Depends、Pydantic 校验模型、OpenAPI Schema 和复杂依赖组合。

相关推荐
SunnyDays10111 小时前
使用 Python 提取 Word 文档中的表格数据
python·word
for_ever_love__1 小时前
python基础语法学习: 动态类型
开发语言·python·学习
Zenova EdgeOS1 小时前
工业网关数据持久化:从同步到 WAL 的工程实战
网络·数据库·oracle
troy1281 小时前
分布式系统框架选型指南:主流框架优缺点深度对比
python·django·pygame
129Lab1 小时前
BMS算法快速原型开发:AI辅助实现扩展卡尔曼滤波(EKF)SOC估算从理论到代码落地
python·嵌入式开发·bms·卡尔曼滤波·电池管理系统·soc估算
萧鼎1 小时前
safetensors 库的安装、核心语法与实战用法,安全保存模型权重
python·开源·教程·python库·safetensors
千里码aicood2 小时前
基于Python语言的测量程序设计
开发语言·python
2601_962300812 小时前
用PyTorch Lightning简化空间分析:Python和机器学习的力量
python·深度学习·机器学习·空间分析·pytorchlightning
jayson.h2 小时前
python-可视化提取表格-通用
开发语言·python