Python Web

类型提示

Java是强类型语言——定义变量、写方法参数时,必须显式声明类型:

```
// Java:类型是强制的
String name = "张三";
int age = 25;

public User login(String username, String password) {
    // ...
}
```

而学习Python语法时会发现,Python里不管是变量还是方法参数,都不需要写类型

```
# Python:不需要写类型
name = "张三"
age = 25

def login(username, password):
    # ...
    pass
```

Python不写类型带来的便利:

  • 写起来快,代码简洁
  • 灵活,同一个变量可以先存字符串再存数字(虽然不推荐)

但不写类型也带来了潜在的风险:

来看一个例子。假设有这样一个函数:

```
def calculate_total(price, count):
    return price * count
```

光看这个函数,你能一眼说清楚 price 和 count 应该传什么吗?

  • price 是整数还是小数?
  • count 是数字,还是可以传一个列表?
  • 返回值又是什么类型?

如果调用的时候不小心传错了:

```
result = calculate_total("100", 3)   # price传成了字符串"100"
# 结果:result = "100100100"(字符串重复3次,不是300!)
```

这种错误在Python里不会在写代码时报错,要等程序真正运行到这一行才会出问题(甚至像上面这样默默算出一个错误结果,连报错都没有)。在Java里,编译期就会直接报错,根本运行不起来。

在大型项目中,没有类型提示的问题会被放大。 上面只是一个3行的小函数,你还能勉强猜出参数含义。但真实项目里有成百上千个函数、几十个互相调用的模块,这时候没有类型提示会非常痛苦。

代码规模越大、协作的人越多,这些问题就越严重。所以在工程实践中,大型Python项目几乎都会要求写类型提示——它不是可有可无的装饰,而是让大项目可维护的重要手段。

为了既保留Python的灵活,又能获得类似Java的”类型提示”帮助,Python引入了类型提示(Type Hints)

类型提示语法

变量类型提示的语法是:在变量名后面加 : 类型

```
# 语法:变量名: 类型 = 值
name: str = "张三"          # name 是字符串
age: int = 25               # age 是整数
price: float = 9.9          # price 是浮点数
is_active: bool = True      # is_active 是布尔值
```

常用的基本类型:

类型含义对应Java
str字符串String
int整数int / Integer
float浮点数double
bool布尔值boolean
list列表List
dict字典Map
Any任意类型Object(约等于)

其中 Any 比较特殊,需要单独说明一下。

Any 表示”任意类型”——标注为 Any 的变量或参数,可以是任何类型,类型检查工具对它不做任何检查。它相当于”我暂时不想(或没法)标注具体类型,先放着”。

```
from typing import Any   # Any 需要从 typing 模块导入

def handle(data: Any) -> Any:
    # data 可以是任何类型,传字符串、数字、列表、字典都行
    ...
```

什么时候用 Any?通常是确实无法确定类型的场景,比如处理一个结构不固定的JSON数据、或者一个第三方库返回的复杂对象。但要注意:Any 用多了就失去了类型提示的意义(因为它等于放弃了类型检查),所以能写具体类型时就尽量写具体类型,Any 只在必要时使用。

对于容器类型,还可以进一步标注里面装的是什么:

```
# list[str]:一个装字符串的列表
names: list[str] = ["张三", "李四"]

# dict[str, int]:一个key是字符串、value是整数的字典
scores: dict[str, int] = {"张三": 90, "李四": 85}
```

方法的类型提示分两部分:参数类型 和 返回值类型

```
# 语法:def 方法名(参数: 参数类型) -> 返回值类型:
def login(username: str, password: str) -> bool:
    # username 和 password 都是字符串,返回一个布尔值
    return username == "admin" and password == "123456"
```
  • 参数类型:在每个参数后面加 : 类型
  • 返回值类型:在参数列表的 ) 后面加 -> 类型

回到前面那个有歧义的例子,加上类型提示后就一目了然了:

```
def calculate_total(price: float, count: int) -> float:
    """price是单价(小数),count是数量(整数),返回总价(小数)"""
    return price * count
```

现在任何人看到这个函数,都能立刻明白:price 要传小数、count 要传整数、返回的是小数。如果用IDE(如PyCharm)传错了类型,编辑器会直接给出黄色波浪线警告。

类型提示只是”提示”

这是和Java最大的区别,必须强调一下:

Python的类型提示仅仅是”提示”,不是”强制”。 它不会在运行时真正检查类型,只是给开发者和IDE看的”说明书”。

```
def login(username: str, password: str) -> bool:
    return True

# 即使把int传给标注为str的参数,Python运行时也不会报错!
result = login(123, 456)   # 能正常运行,不报错
```

类型提示的真正价值在于:

  • 给人看:让代码可读性更强,别人(和几个月后的你)一看就知道该传什么
  • 给IDE看:PyCharm、VSCode 能据此做自动补全、错误高亮
  • 给工具看:配合 mypy 这类静态检查工具,可以在运行前发现类型错误

项目中的类型提示

在后续项目代码里,会看到大量类型提示,例如:

```
def chat_history(thread_id: str) -> RedisChatMessageHistory:
    # 参数 thread_id 是字符串,返回一个 RedisChatMessageHistory 对象
    ...

def create_multi_query_retriever(top_k: int) -> MultiQueryRetriever:
    # 参数 top_k 是整数,返回一个 MultiQueryRetriever 对象
    ...
```

看到这些类型提示,就能快速理解每个函数”要什么、给什么”,这对阅读项目代码非常有帮助。

@dataclass 装饰器

在 Java 里,我们经常写一种”只用来装数据”的类——POJO/实体类,比如:

```
public class User {
    private String name;
    private int age;
    private String city;
    // 构造方法、getter、setter、toString、equals... 一大堆样板代码
}
```

这种类除了几个字段,剩下的全是样板代码(构造方法、toStringequals 等)。Java 里我们用 Lombok 的 @Data 注解来自动生成这些样板代码,省得手写。

Python 里也有同样的需求——经常要定义”只用来装数据”的类。如果手写,是这样的:

```
class User:
    def __init__(self, name, age, city):
        self.name = name      # 每个字段都要在 __init__ 里手动赋值一遍
        self.age = age
        self.city = city

    def __repr__(self):       # 想打印好看,还得自己写 __repr__
        return f"User(name={self.name}, age={self.age}, city={self.city})"
```

字段一多,这种”self.xxx = xxx“的重复赋值就很啰嗦。Python 提供了 @dataclass 装饰器来解决这个问题——它的作用就相当于 Java 的 Lombok @Data

@dataclass 来自标准库 dataclasses,用法是:在类上面加 @dataclass 装饰器,然后用”字段名: 类型”的方式声明字段即可:

```
from dataclasses import dataclass

@dataclass
class User:
    name: str       # 直接声明字段名和类型,不用写 __init__
    age: int
    city: str
```

就这么几行,@dataclass 会自动帮我们生成

  • __init__ 构造方法(参数就是上面声明的那些字段)
  • __repr__(打印时显示成 User(name='张三', age=25, city='杭州')
  • __eq__(两个对象字段都相等时,== 返回 True)

用起来和普通类完全一样:

```
# 自动生成了 __init__,可以直接按字段顺序创建对象
u = User("张三", 25, "杭州")

# 访问字段
print(u.name)    # 张三
print(u.age)     # 25

# 自动生成了 __repr__,打印很友好
print(u)         # User(name='张三', age=25, city='杭州')

# 自动生成了 __eq__,字段都相等就判定相等
u2 = User("张三", 25, "杭州")
print(u == u2)   # True
```

和普通函数参数一样,可以给字段加默认值。注意:有默认值的字段必须排在没默认值的字段后面(和函数参数规则一致)。

```
from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int
    city: str = "杭州"     # 默认值,创建对象时可以不传

u = User("李四", 30)       # 没传 city,用默认值
print(u)                   # User(name='李四', age=30, city='杭州')
```

@dataclass 还可以加参数。最常用的是 frozen=True,它会让生成的对象不可修改(类似 Java 里给字段都加 final):

```
from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: int
    y: int

p = Point(1, 2)
print(p.x)        # 1,可以读

p.x = 10          # ❌ 报错!frozen 对象不允许修改字段
# dataclasses.FrozenInstanceError: cannot assign to field 'x'
```

什么时候用 frozen=True?当这个数据对象创建后就不应该再被改动时(比如配置项、坐标点这类”值对象”),用它能防止误改,让代码更安全。后续项目代码里你会看到 @dataclass(frozen=True) 这种写法,就是在定义这种只读数据对象。

强制关键字参数

在调用Python函数时,传参有两种方式:

```
def create_user(name, age, city):
    print(f"{name}, {age}岁, 来自{city}")

# 方式1:位置参数 —— 按位置顺序传,第1个给name,第2个给age...
create_user("张三", 25, "杭州")

# 方式2:关键字参数 —— 明确写出参数名,顺序无所谓
create_user(name="张三", age=25, city="杭州")
create_user(city="杭州", name="张三", age=25)   # 顺序打乱也没关系
```

除了上面两种,Python还有第三种参数定义方式——强制关键字参数:强制要求调用者必须用”参数名=值”的形式传参,不允许用位置传参。

语法是:在参数列表里加一个 ** 后面的所有参数都变成强制关键字参数

```
# * 后面的 age 和 city 变成强制关键字参数
def create_user(name, *, age, city):
    print(f"{name}, {age}岁, 来自{city}")

# ✅ 正确:age 和 city 必须写参数名
create_user("张三", age=25, city="杭州")

# ❌ 错误:age 和 city 不能用位置传参
create_user("张三", 25, "杭州")   # 报错:TypeError
```

注意 * 本身不是一个参数,它只是一个”分界线”:

  • * 前面的参数(如 name):正常参数,位置传/关键字传都行
  • * 后面的参数(如 agecity):强制关键字参数,必须写参数名

这样做的好处是提升可读性、避免传参出错——当参数很多(尤其是有多个布尔值或数字)时,强制写参数名能让调用代码一目了然,不会因为顺序问题把参数传反。

项目的约会规划代码里就用到了这个语法:

```
def _research_recommendations(
        state,
        *,                          # 这个星号后面全是强制关键字参数
        query_terms,
        prompt_factory,
        output_key,
        found_key,
        include_weather=False,
):
    ...

# 调用时,星号后面的参数必须写参数名
_research_recommendations(
    state,
    query_terms="...",          # 必须写 query_terms=
    prompt_factory=...,         # 必须写 prompt_factory=
    output_key="activity_recommendations",
    found_key="activity_search_found",
    include_weather=True,
)
```

虚拟环境

我们可以通过pip在当前项目中引入所需的依赖,但是,pip这个工具远没有JAVA中的Maven那么强大,从依赖管理的角度来说,pip 不支持同一个库装多个版本。这意味着对同一个 pip 依赖目录(也就是库的安装目录)来说,一个库(比如 langchain)在同一时刻只能存在一个版本。你 pip install langchain==1.0,它就会把原来的 langchain==0.1 覆盖掉——不是两个版本并存,而是后装的把先装的顶替了。

这个特性在只有一个项目时没问题,但当你的电脑上有多个项目、它们需要的库版本不一样时,麻烦就来了。设想你同时开发两个Python项目:

  • 项目A:一个老项目,依赖 langchain 的 0.1 版本
  • 项目B:我们这个新项目,依赖 langchain 的 1.0 版本

如果这些库都装在同一个 pip 依赖目录里(比如直接装在你安装Python时那个默认的依赖目录里,所有项目都共用它),就会出现冲突:

```
你为项目B装了 langchain==1.0
   ↓
因为 pip 不支持多版本,项目A原来的 langchain==0.1 被覆盖了
   ↓
项目A跑不起来了!
```

项目A要0.1、项目B要1.0,但共用的这个依赖目录里 langchain 只能留一个版本,两者无法共存——这就是”依赖冲突”。

类比Java:这就好比所有Maven项目共用同一个 lib 目录,A项目要 fastjson 1.x,B项目要 fastjson 2.x,但目录里同一个jar只能放一个版本,必然打架。(Java实际上靠Maven的本地仓库 + 每个项目独立的依赖声明避免了这个问题。)

虚拟环境是什么

为了解决这个问题,Python提供了虚拟环境(Virtual Environment)

核心思想:给每个项目创建一个独立的、隔离的 pip 依赖目录。 每个项目有自己专属的一个依赖目录,互不影响:

```
项目A 的虚拟环境 → 依赖目录里装着 langchain==0.1
项目B 的虚拟环境 → 依赖目录里装着 langchain==1.0
两个依赖目录互相隔离,谁也不影响谁
```

这样,项目A用它自己依赖目录里的0.1,项目B用它自己依赖目录里的1.0,虽然 pip 还是不支持多版本,但因为两个版本分别装在不同的依赖目录里,就不会互相覆盖了。

虚拟环境的原理

虚拟环境的实现原理其实很朴素——它本质上就是一个独立的文件夹

当你为项目创建一个虚拟环境时,会在项目目录下生成一个文件夹(通常叫 .venv 或 venv),这个文件夹里最关键的东西是:

  • 一个独立的 site-packages 目录——这就是前面一直说的”pip 依赖目录”,这个项目 pip install 装的所有库都进到这里,而不是装到Python默认的那个依赖目录里
  • 一些激活脚本和一个指向系统Python解释器的入口

这里要澄清一个常见的误解:虚拟环境并不会复制一份完整的Python解释器。 真正干活的Python解释器还是你电脑上安装的那个,虚拟环境只是建了一个”轻量的入口”指向它。虚拟环境真正隔离的、每个项目各自独立的,是依赖目录(site-packages——也就是”装哪些库、装什么版本”这部分。

当你”激活”这个虚拟环境后,命令行里的 python 和 pip 命令就会改用这个文件夹里的配置。于是:

  • pip install xxx → 装到当前项目虚拟环境的 site-packages 依赖目录里
  • import xxx → 从当前项目虚拟环境的 site-packages 依赖目录里找

每个项目一个文件夹、各自一个依赖目录,文件夹之间天然隔离,这就实现了”每个项目一个独立的 pip 依赖目录”。

在PyCharm中使用虚拟环境

实际开发中我们用PyCharm,它已经把虚拟环境的创建和激活都做好了,基本不用手敲命令。

方式一:新建项目时自动创建

用PyCharm新建一个Python项目时,在创建界面里会有”Python解释器”相关的选项,默认就是 New environment(新建虚拟环境),类型选 Virtualenv。保持默认即可——PyCharm会自动在项目目录下创建一个 .venv 文件夹作为这个项目的虚拟环境。

方式二:为已有项目配置虚拟环境

打开我们这个项目后,需要确认/配置它用的解释器:

  1. 打开 File → Settings(macOS 是 PyCharm → Preferences
  2. 进入 Project: 项目名 → Python Interpreter
  3. 如果项目里已经有 .venv,PyCharm通常会自动识别;如果没有,点右上角齿轮/Add Interpreter → Add Local Interpreter → 选 Virtualenv Environment → New,PyCharm就会创建一个新的虚拟环境

配置好之后,PyCharm会自动激活这个虚拟环境——你打开PyCharm下方的 Terminal,会看到命令行前面带着 (.venv) 字样,表示当前就在这个虚拟环境里。此时直接安装项目依赖即可:

```
pip install -r requirements.txt
```

装的所有库都会进入这个项目专属的 .venv,不会影响别的项目。

理解了背后的原理——”每个项目一个独立文件夹存自己的库”——能帮你搞清楚为什么有时候”明明装了库却 import 不到”:往往是你装库时用的环境,和PyCharm里项目配置的解释器不是同一个,库装到别的环境去了。

小结

问题虚拟环境的解决方式
pip不支持同一个库多版本,多项目版本需求冲突每个项目一个独立的 pip 依赖目录,各装各的版本,分别隔离
不知道库装哪了都装在项目自己的 .venv 文件夹里
环境互相污染文件夹隔离,互不影响

在项目里,第一步就应该先用PyCharm为项目配置好虚拟环境,然后再 pip install -r requirements.txt 安装项目依赖。

利用Python语言,如何实现后端功能呢?要实现一个完整的后端服务,我们需要从两个层面来考虑:

  1. Web服务器层面:解决”如何接收请求”的问题
  2. Web应用层面:解决”如何处理请求”的问题

这要解决了这两个层面的问题,我们就可以接收到请求,并且能处理请求,后端功能自然就可以实现

Web服务器层面

在学习Python的Web服务器之前,先回顾一下Java中最熟悉的Web服务器——Tomcat,我么来回顾一下它的一次请求响应过程:

  • 首先,有一个前提条件就是我们得先把我们的web应用部署到Tomcat中
  • 客户端发送一个HTTP请求,首先是被Tomcat服务器接收到,他将HTTP请求报文解析为Request对象
  • 通过servlet-mapping找到目标web应用的目标servlet,调用其service(reqeuest, resposne)方法将请求交给Servlet
  • Servlet处理完请求后,将数据放入Response,Tomcat将Response封装成HTTP响应报文返回给客户端

总结一下我们熟悉的Tomcat功能:

  • Tomcat服务器上可以部署Web应用,而且不止一个
  • 通过Servlet机制,Tomcat服务器最终找到处理请求的Servlet,并将封装好的请求传递给它
  • Tomcat服务器接收HTTP请求,返回HTTP响应
  • Tomcat服务器还支持Cookie、Session、Listener、Filter等机制

问题来了,Python中是否有类似的Web服务器?

有,但和Tomcat不完全等价。 在我们的项目中所使用的是一个名为 Uvicorn 的Web服务器。

Uvicorn服务器

Uvicorn与Tomcat的对比,相同点是:

  • 都可以部署Web应用
  • 都可以接收HTTP请求
  • 都可以返回HTTP响应

同时,它们也有很多不同点,罗列如下:

Uvicorn可以认为是Tomcat的瘦身版,它的设计理念完全不同:

特性TomcatUvicorn
部署应用数量支持多个Web应用只支持一个Web应用
Servlet机制✅ 支持❌ 不支持
Cookie/Session✅ 支持❌ 不支持
Filter/Listener✅ 支持❌ 不支持
核心职责全功能Web容器高效传输数据

Uvicorn的设计哲学:

  • Uvicorn除了支持基本的通信协议之外,其他的类似Servlet、Cookie、Session这样的机制统统不支持
  • 它的唯一目标就是:追求基于传输协议高效地传输数据
  • 至于请求数据接收到之后交给谁处理,它不知道也不关心

Uvicorn的数据处理

虽然Uvicorn不关心数据交给谁,但是Uvicorn无法单独启动,必须得有人能接收数据才行,否则,Uvicorn服务器运行起来没有任何意义。

Uvicorn在接收到数据后会调用python中的一种特殊的函数——异步函数,这个异步函数对于Uvicorn而言就相当于数据接收者

异步函数的定义方式: 在普通函数前加上 async 关键字

接下来,我们可以自己定义一个简单的异步函数,来接收Uvicorn服务器所接收到的请求,并返回一个简单的字符串

```
# 文件名: simple_server.py

# 定义一个异步函数,用于接收Uvicorn传递过来的请求
async def app(scope, receive, send):
    """
    这是Uvicorn要求的异步函数签名
    
    参数说明:
    - scope: 包含请求的基本信息(请求路径、方法、头部等)
    - receive: 用于接收请求体数据的函数
    - send: 用于发送响应数据的函数
    """
    
    # 发送HTTP响应的起始行和响应头
    await send({
        'type': 'http.response.start',  # 响应开始
        'status': 200,                   # HTTP状态码 200 表示成功
        'headers': [
            [b'content-type', b'text/plain'],  # 响应内容类型为纯文本
        ],
    })
    
    # 发送HTTP响应体
    await send({
        'type': 'http.response.body',           # 响应体
        'body': b'Hello from Uvicorn!',         # 响应内容(必须是字节类型)
    })
```

代码解析:

  1. async def app(...): 定义异步函数,函数名可以任意,但参数必须是 scope, receive, send
  2. await send(...)await 关键字表示”等待”这个异步操作完成
  3. 响应分两步发送:
  • 第一步:发送响应状态码和响应头
  • 第二步:发送响应体内容

一个常见疑问:响应头和响应体能不能合成一条 send 一起发?

不能。在 ASGI(Uvicorn 所遵循的协议)里,响应头和响应体必须分成两条消息发送,这是协议的硬性规定——不是写法上的可选项:

  • type 为 http.response.start 的消息:只携带状态码(status)和响应头(headers),不能带响应体
  • type 为 http.response.body 的消息:只携带响应体(body),不能带状态码和响应头

为什么要这样拆开? 有两个原因:

  1. HTTP 响应本身就是”先头后体”的顺序结构:一条 HTTP 响应报文,永远是先有响应行+响应头,然后才是响应体。ASGI 用两条消息正好对应这个先后顺序。
  2. 为了支持流式响应:响应体可以分很多次发送——比如返回一个大文件、或者像后面会讲的 SSE 流式输出,响应体是一段一段陆续产生的。这种情况下,http.response.start(头)只发一次,http.response.body(体)则可以发很多次,每次发一小块。如果把头和体绑死在一条消息里,就没法实现这种”头发一次、体发多次”的流式效果了。

实际上,发送多个 http.response.body 时,可以用一个 more_body 字段标记”后面还有没有更多内容”:

```
# 流式发送响应体示意(后面还有内容时 more_body=True)
await send({'type': 'http.response.body', 'body': b'第一块', 'more_body': True})
await send({'type': 'http.response.body', 'body': b'第二块', 'more_body': True})
await send({'type': 'http.response.body', 'body': b'最后一块', 'more_body': False})  # 结束
```

我们上面的例子只发一块响应体、不需要流式,所以省略了 more_body(默认就是 False,表示发完了)。

不用担心——后面我们用 FastAPI 时,这些底层的”分两步发送”细节都被框架封装好了,我们只管 return 返回值即可。这里了解一下原理就行。

启动Uvicorn服务器

首先需要安装服务器所需的依赖,在项目目录下打开终端,执行以下命令:

```
pip install uvicorn
```

接着,就可以启动uvicorn服务器了,有两种启动方式:

第一种方式,直接在终端中执行:

```
uvicorn simple_server:app [--reload] [--port=xxxx]
```

命令解析:

  • uvicorn: Uvicorn命令
  • simple_server: Python文件名(不带.py后缀)
  • app: 文件中定义的异步函数名
  • --reload: 开启热重载,代码修改后自动重启服务器(开发时使用)
  • --port: 指定服务器启动的端口,如果未指定那么默认8000端口

启动成功后,你会看到类似输出:

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

第二种方式

  1. 创建一个新的Python文件,例如 run_server.py
```
# 文件名: run_server.py

import uvicorn
import uvicorn
app = "simple_server:app"

if __name__ == "__main__":
    # 启动Uvicorn服务器
    uvicorn.run(
        app,                    # 传入我们定义的异步函数
        host="127.0.0.1",       # 监听的IP地址
        port=8000,              # 监听的端口号
        reload=True             # 开启热重载
    )
```
  1. 在PyCharm中运行 run_server.py

不管通过哪种方式,只要启动成功后打开浏览器访问:

```
http://127.0.0.1:8000
```

你应该能看到页面显示:

```
Hello from Uvicorn!
```

Web应用层面

通过上一节的学习,我们已经知道要使用Uvicorn服务器,就必须自己定义一个异步函数真正处理所接收的请求

那么问题来了,如果要让我们自己真的来实现这样一个函数,我们需要在这个函数中实现什么功能?

让我们一起分析一下,至少有以下四点需要实现:

  1. 区分不同的请求: 一个Web应用所要处理的请求肯定不止一个,比如用户访问 /login 和 /register 应该执行不同的逻辑,我们需要根据请求路径、请求方法(GET/POST)来区分请求
  2. 解析请求参数: 请求中可能携带路径参数,可能携带key-value形式的查询参数,也可能携带请求体参数(如POST请求的JSON数据),我们需要从请求中解析这些参数
  3. 实现业务逻辑: 根据不同的请求,调用不同的业务处理代码,可能需要查询数据库、调用AI接口、处理文件等
  4. 封装响应:将处理结果转换为JSON格式,设置正确的响应头,返回给客户端

如果我们自己实现,代码可能是这样的:

```
async def app(scope, receive, send):
    # 1. 解析请求路径
    path = scope['path']
    method = scope['method']
    
    # 2. 根据路径和方法分发请求
    if path == '/login' and method == 'POST':
        # 解析请求体中的用户名和密码
        body = await receive()
        username = parse_param(body, 'username')
        password = parse_param(body, 'password')
        # 执行登录逻辑
        result = do_login(username, password)
        # 封装JSON响应
        response = json_response(result)
        await send(response)
    
    elif path.startswith('/user/') and method == 'GET':
        # 解析路径参数
        user_id = parse_path_param(path, '/user/')
        # 查询用户信息
        user = get_user(user_id)
        # 封装JSON响应
        response = json_response(user)
        await send(response)
    
    # ... 还有很多其他请求需要处理
```

问题又来了,你愿意这样写代码吗?当然不愿意!

因为不管是区分不同的请求、用不同的方法处理它们,还是解析请求中的参数,这些在Java语言中,不都相当于是SpringMVC在做的事情吗?

我们真正需要的应该是像使用SpringMVC一样,绑定请求和对应的处理方法,一个方法处理一个请求!

```
@RestController
public class UserController {
    
    // 1. 定义方法和请求的映射
    @PostMapping("/login")
    public Result login(
        // 2. 通过方法参数接收请求参数
        @RequestParam String username,
        @RequestParam String password
    ) {
        // 3. 实现业务逻辑
        User user = userService.login(username, password);
        // 4. 方法直接返回对象,自动被转化为JSON放入响应体
        return Result.success(user);
    }
    
    @GetMapping("/user/{id}")
    public Result getUser(@PathVariable Long id) {
        User user = userService.getUser(id);
        return Result.success(user);
    }
}
```

在Python中有没有办法实现这样的效果?有, 这就是我们要学习的 FastAPI 框架。

FastAPI

FastAPI 是一个现代、快速(高性能)的Web框架,用于基于Python构建API,它的功能类比于SpringMVC

其实FastAPI的本质就是用来在Uvicorn中接收数据的那个异步函数,同时它支持像SpringMVC一样,让我们自己定义不同的方法处理不同的请求。

FastAPI在中间层做了什么?

  • 接收Uvicorn传递的原始请求数据
  • 根据请求路径和方法,路由到对应的处理函数
  • 自动解析请求参数,注入到函数参数中
  • 执行我们定义的业务逻辑函数
  • 将函数返回值自动转换为JSON响应
  • 将响应数据传递给Uvicorn返回给客户端

接下来,我们可以类比SpringMVC学习FastAPI的使用

创建FastAPI应用

首先,我们需要执行如下命令,安装FastAPI依赖

```
pip install fastapi
```

接着,我们就可以创建FastAPI应用(其实也就是实现自己的web应用)

```
# 文件名: main.py

# 1. 导入FastAPI类
from fastapi import FastAPI

# 2. 创建FastAPI应用实例
app = FastAPI()
```

代码解析:

  • from fastapi import FastAPI: 从fastapi模块导入FastAPI类
  • app = FastAPI(): 创建一个FastAPI应用实例
  • 这个 app 对象就是我们的Web应用
  • 它负责管理所有的路由(URL到处理函数的映射)
  • 它实现了Uvicorn所需要的异步函数接口
  • 我们后续所有的路由注册、配置等都通过这个对象来完成

在FastAPI中,app = FastAPI() 就相当于创建了整个应用的核心对象。

注册路由

类比SpringMVC的Mapping定义

在SpringMVC中,我们使用注解来定义路由:

```
@GetMapping("/hello")
public String hello() {
    return "Hello World";
}
```

在Python中,没有Java那样的注解,但是有一个类似的语法叫做装饰器(Decorator)。装饰器以 @ 开头,写在函数上方,作用与注解非常相似。

```
from fastapi import FastAPI

app = FastAPI()

# 使用装饰器注册路由
# @app.get("/") 表示:将HTTP GET方法、路径为"/"的请求,绑定到下面的函数
@app.get("/")
def hello():
    """这是一个简单的接口,不接收参数,返回常量字符串"""
    return "Hello FastAPI"

if __name__ == "__main__":
    # 启动Uvicorn服务器
    uvicorn.run(
        app,                    # 传入fastAPI的app函数
        host="127.0.0.1",       # 监听的IP地址
        port=8000,              # 监听的端口号
        reload=True             # 开启热重载
    )
```

代码解析:

  • @app.get("/"): 装饰器,告诉FastAPI:
  • HTTP方法:GET
  • 请求路径:/
  • 处理函数:紧跟在下面定义的函数(hello
  • def hello(): 定义处理函数
  • return "Hello FastAPI": 返回响应内容

启动后访问 http://127.0.0.1:8000,你会看到:

```
"Hello FastAPI"
```

FastAPI的常用装饰器,以及与SpringMVC的对应关系如下表

HTTP方法SpringMVCFastAPI
GET@GetMapping("/path")@app.get("/path")
POST@PostMapping("/path")@app.post("/path")
PUT@PutMapping("/path")@app.put("/path")
DELETE@DeleteMapping("/path")@app.delete("/path")

注册路由还有另外一种方式——APIRouter。前面的写法是把路由直接注册到 app 上@app.get(...))。当接口很少时这没问题,但随着接口越来越多,全部堆在一个文件、注册到同一个 app 上,代码会变得很乱。

回想一下SpringMVC——我们不会把所有接口都写在一个Controller里,而是按业务拆成多个 @RestController(比如 UserControllerOrderController)。FastAPI 也提供了类似的机制:APIRouter

APIRouter 可以理解为一个”子路由器”——先把一组相关的接口注册到这个子路由器上,最后再统一挂载到主 app。它的对应关系是:

FastAPISpringMVC作用
APIRouter一个 @RestController承载一组相关接口
app.include_router(...)容器扫描并注册Controller把子路由挂载到主应用

第一步:用 APIRouter 定义一组接口

```
# 文件名: user_routes.py

from fastapi import APIRouter

# 创建一个子路由器,prefix 指定这组接口的公共路径前缀
router = APIRouter(prefix="/user")

# 注意:这里用的是 @router 而不是 @app
@router.get("/hello")
def hello():
    return "Hello from user router"

@router.get("/profile")
def profile():
    return {"name": "张三", "age": 25}
```

关键点:prefix 会自动加在每个接口路径前面。 所以上面两个接口最终的访问路径是:

  • @router.get("/hello") → 实际路径 /user/hello(prefix + 路径)
  • @router.get("/profile") → 实际路径 /user/profile

第二步:在主应用中挂载这个子路由器

```
# 文件名: main.py

from fastapi import FastAPI
from user_routes import router as user_router

app = FastAPI()

# 把子路由器挂载到主应用上
app.include_router(user_router)
```

挂载之后,启动服务访问 http://127.0.0.1:8000/user/hello,就能访问到 user_routes.py 里定义的接口了。

这样拆分的好处: 用户相关的接口写在 user_routes.py,订单相关的接口写在 order_routes.py,每个文件各管一摊,主应用 main.py 只负责把它们 include_router 进来,代码结构清晰,便于多人协作和后期维护。

两种注册方式对比:

方式写法适用场景
直接注册@app.get(...)接口很少、单文件的小项目
子路由APIRouter + app.include_router(...)按业务模块拆分的多文件项目

我们的项目接口较多(情感咨询、约会规划等),采用的就是 APIRouter 的方式。

pycharm中运行FastAPI项目

为了运行我们基于FastAPI实现的web项目,需要写如下代码:

```
if __name__ == "__main__":
    # 启动Uvicorn服务器
    uvicorn.run(
        app,                    # 传入fastAPI的app函数
        host="127.0.0.1",       # 监听的IP地址
        port=8000,              # 监听的端口号
        reload=True             # 开启热重载
    )
```

但是,在PyCharm中其实不用这么麻烦,我们只需要创建专门的FastAPI类型的项目

创建项目之后的效果如下图所示:

同时,我们可以修改项目的启动脚本:

启动之后的效果如下:

接收请求参数

在Web开发中,请求参数主要有三种形式:

  1. 路径参数(Path Parameter):作为URL路径的一部分
  2. 查询参数(Query Parameter):URL中?后面的key-value形式参数
  3. 请求体参数(Request Body Parameter):放在HTTP请求体中的参数(通常是JSON)

让我们一一来看FastAPI如何接收这三种参数。

接收路径参数

场景: 根据用户ID查询用户信息

对比SpringMVC:

```
@GetMapping("/user/{user_id}")
public User getUser(@PathVariable Long user_id) {
    return userService.getUser(user_id);
}
```

FastAPI实现:

```
@app.get("/user/{user_id}")
def get_user(user_id: int):
    """
    根据用户ID获取用户信息
    
    路径参数:user_id - 用户ID
    """
    return {"user_id": user_id, "name": f"用户{user_id}"}
```

代码解析:

  • /user/{user_id}: 路径中用 {user_id} 表示这是一个路径参数
  • def get_user(user_id: int): 函数参数名必须与路径中的占位符完全相同
  • user_id: int: 这是Python的类型注解,告诉FastAPI这个参数应该是整数类型
  • FastAPI会自动将URL中的字符串转换为整数
  • 如果转换失败(如传入字符串),会自动返回错误响应

测试: 访问 http://127.0.0.1:8000/user/123,返回:

```
{"user_id": 123, "name": "用户123"}
```

接收查询参数

场景: 搜索功能,关键词作为查询参数

对比SpringMVC:

```
@GetMapping("/search")
public Result search(@RequestParam String keyword, @RequestParam(defaultValue = "10") Integer pageSize) {
    return searchService.search(keyword, pageSize);
}
```

FastAPI实现:

```
@app.get("/search")
def search(keyword: str, page_size: int = 10):
    """
    搜索接口
    
    查询参数:
    - keyword: 搜索关键词(必填)
    - page_size: 每页大小(可选,默认10)
    """
    return {
        "keyword": keyword,
        "page_size": page_size,
        "results": [f"结果{i}" for i in range(page_size)]
    }
```

代码解析:

  • keyword: str: 必填参数,没有默认值
  • page_size: int = 10: 可选参数,默认值为10
  • FastAPI会自动从URL的查询字符串中提取这些参数

接收请求体参数

在接收请求体参数之前,我们需要先了解一个重要的库:Pydantic

它是一个数据验证库,用于定义数据模型。

类比Java:

在Java中,我们会定义一个实体类:

```
public class User {
    private String username;
    private String password;
    private Integer age;
    
    // getters and setters...
}
```

在Python中,我们使用Pydantic定义数据模型:

```
from pydantic import BaseModel

class User(BaseModel):
    username: str
    password: str
    age: int
```

Pydantic的优势:

  • 自动进行数据验证(类型检查、必填检查等)
  • 自动将JSON转换为Python对象
  • 自动将Python对象转换为JSON
  • 如果转化失败,提供清晰的错误信息

Pydantic 和 @dataclass 有什么区别?该用哪个?

前面我们学过 @dataclass,它也能定义”只装数据”的类,看起来和 Pydantic 的 BaseModel 很像:

```
# @dataclass 写法
from dataclasses import dataclass

@dataclass
class User:
    username: str
    age: int

# Pydantic 写法
from pydantic import BaseModel

class User(BaseModel):
    username: str
    age: int
```

两者长得几乎一样,但有一个关键区别——是否做数据校验

先看两者作为普通数据类自身的能力对比:

@dataclassPydantic BaseModel
来源Python 标准库第三方库(需 pip install pydantic
自动生成构造方法等样板
运行时校验类型❌ 不校验✅ 会校验
类型不符时照样赋值,不报错抛出校验错误

举个例子看区别——给 age(声明为 int)传一个字符串:

```
# @dataclass:类型提示只是"提示",自身不校验。传字符串照样塞进去,不报错
u1 = User_dataclass(username="张三", age="二十五")   # 正常执行,age 就是字符串"二十五"

# Pydantic:会真正校验类型。传不能转成 int 的值,直接抛错
u2 = User_pydantic(username="张三", age="二十五")    # ❌ 抛出 ValidationError
```

简单记:两者在 FastAPI 里都能接收请求体,但外部输入要做严格校验,优先用 Pydantic;内部可信数据流转,用 @dataclass 更轻量。

接收请求体参数

场景: 用户注册,提交用户信息

对比SpringMVC:

```
@PostMapping("/register")
public Result register(@RequestBody User user) {
    return userService.register(user);
}
```

FastAPI实现:

```
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

# 1. 定义请求体数据模型
class UserRegister(BaseModel):
    """用户注册请求模型"""
    username: str      # 用户名,字符串类型,必填
    password: str      # 密码,字符串类型,必填
    age: int          # 年龄,整数类型,必填
    email: str = ""   # 邮箱,字符串类型,可选(默认空字符串)

# 2. 定义接口,接收请求体参数
@app.post("/register")
def register(user: UserRegister):
    """
    用户注册接口
    
    请求体参数:user - 用户注册信息(JSON格式)
    """
    return user.username
```

代码解析:

  1. 定义数据模型:
  • class UserRegister(BaseModel): 继承自Pydantic的BaseModel
  • 类中的每个属性都有类型注解
  • 可以设置默认值(如 email: str = ""
  1. 接收请求体:
  • def register(user: UserRegister): 函数参数类型为我们定义的模型
  • FastAPI会自动:
  • 读取请求体中的JSON数据
  • 验证数据类型是否正确
  • 将JSON转换为UserRegister对象
  • 如果验证失败,自动返回400错误和详细的错误信息
  1. 访问数据:
  • user.username: 通过对象属性访问数据
  • user.age: 访问其他属性

使用Postman或curl发送POST请求

```
curl -X POST "http://127.0.0.1:8000/register" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "zhangsan",
    "password": "123456",
    "age": 25,
    "email": "zhangsan@example.com"
  }'
```

返回:

```
{
  "message": "注册成功",
  "username": "zhangsan",
  "age": 25,
  "email": "zhangsan@example.com"
}
```

返回JSON响应

在FastAPI中,返回JSON非常简单,只需要返回Python的字典或对象即可,通常有以下

方式一:返回字典

```
@app.get("/api/info")
def get_info():
    """返回字典,FastAPI自动转换为JSON"""
    return {
        "code": 200,
        "message": "success",
        "data": {
            "name": "FastAPI",
            "version": "0.100.0"
        }
    }
```

方式二:返回Pydantic模型

```
from pydantic import BaseModel

# 定义响应模型
class UserResponse(BaseModel):
    user_id: int
    username: str
    age: int

@app.get("/api/user/{user_id}")
def get_user_info(user_id: int):
    """返回Pydantic模型对象,FastAPI自动转换为JSON"""
    # 模拟从数据库查询用户
    user = UserResponse(
        user_id=user_id,
        username=f"user_{user_id}",
        age=25
    )
    return user
```

访问 http://127.0.0.1:8000/api/user/123,返回:

```
{
  "user_id": 123,
  "username": "user_123",
  "age": 25
}
```

方式三:返回列表

```
@app.get("/api/users")
def get_users():
    """返回列表,FastAPI自动转换为JSON数组"""
    return [
        {"user_id": 1, "username": "user1"},
        {"user_id": 2, "username": "user2"},
        {"user_id": 3, "username": "user3"}
    ]
```

方式四:返回 @dataclass 对象

前面讲请求体时我们对比过 Pydantic 和 @dataclass。在返回这一侧,FastAPI 同样支持直接返回 @dataclass 对象——它会被自动转成 JSON:

```
from dataclasses import dataclass

@dataclass
class UserResponse:
    user_id: int
    username: str
    age: int

@app.get("/api/user/{user_id}")
def get_user_info(user_id: int):
    """返回 @dataclass 对象,FastAPI 自动转换为 JSON"""
    return UserResponse(user_id=user_id, username=f"user_{user_id}", age=25)
```

访问后返回:

```
{
  "user_id": 123,
  "username": "user_123",
  "age": 25
}
```

为什么 @dataclass 也能被自动转成 JSON? 因为 FastAPI 在返回前会用一个内部工具 jsonable_encoder 把返回值转成可序列化的结构,它对字典、列表、Pydantic 模型、@dataclass 都有专门处理。所以返回 @dataclass 对象是没问题的。

不过要注意区分返回接收两个方向:

  • 返回@dataclass 和 Pydantic 都能正常转成 JSON,差别不大,按喜好选即可。
  • 接收(请求体):如前所述,因为要对外部输入做严格校验,仍优先用 Pydantic

所以实际项目里,常见的做法是:请求体和响应体都统一用 Pydantic 模型,这样校验、文档生成(FastAPI 能根据 Pydantic 模型自动生成接口文档)都更顺手,风格也统一。

FastAPI的自动转换规则:

  • Python字典 → JSON对象
  • Python列表 → JSON数组
  • Pydantic模型 → JSON对象
  • @dataclass 对象 → JSON对象
  • 基本类型(str, int, float, bool) → 对应的JSON类型

综合运用

让我们通过实现一个简单的用户管理功能呢,综合运用前面学到的所有知识。

```
# 文件名: user_api.py

from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional

# 创建FastAPI应用
app = FastAPI()

# ============ 数据模型定义 ============

class UserCreate(BaseModel):
    """用户创建请求模型"""
    username: str
    password: str
    age: int
    email: str|None = None  # 值可以为None

class UserResponse(BaseModel):
    """用户响应模型"""
    user_id: int
    username: str
    age: int
    email: Optional[str] = None

# ============ 模拟数据库 ============

# 使用字典模拟数据库存储
users_db = {}
next_user_id = 1

# ============ API接口定义 ============

@app.get("/")
def root():
    """根路径,返回欢迎信息"""
    return {"message": "欢迎使用用户管理API"}

@app.post("/users", response_model=UserResponse)
def create_user(user: UserCreate):
    """
    创建用户
    
    请求体参数:
    - username: 用户名
    - password: 密码
    - age: 年龄
    - email: 邮箱(可选)
    """
    global next_user_id
    
    # 创建用户对象
    new_user = UserResponse(
        user_id=next_user_id,
        username=user.username,
        age=user.age,
        email=user.email
    )
    
    # 保存到"数据库"
    users_db[next_user_id] = new_user
    next_user_id += 1
    
    return new_user

@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int):
    """
    根据ID获取用户
    
    路径参数:
    - user_id: 用户ID
    """
    if user_id not in users_db:
        return {"error": "用户不存在"}
    return users_db[user_id]

@app.get("/users")
def list_users(page: int = 1, page_size: int = 10):
    """
    获取用户列表
    
    查询参数:
    - page: 页码(默认1)
    - page_size: 每页大小(默认10)
    """
    all_users = list(users_db.values())
    start = (page - 1) * page_size
    end = start + page_size
    
    return {
        "total": len(all_users),
        "page": page,
        "page_size": page_size,
        "users": all_users[start:end]
    }
```
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇