类型提示
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... 一大堆样板代码
}
```
这种类除了几个字段,剩下的全是样板代码(构造方法、toString、equals 等)。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):正常参数,位置传/关键字传都行*后面的参数(如age、city):强制关键字参数,必须写参数名
这样做的好处是提升可读性、避免传参出错——当参数很多(尤其是有多个布尔值或数字)时,强制写参数名能让调用代码一目了然,不会因为顺序问题把参数传反。
项目的约会规划代码里就用到了这个语法:
```
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 文件夹作为这个项目的虚拟环境。
方式二:为已有项目配置虚拟环境
打开我们这个项目后,需要确认/配置它用的解释器:
- 打开
File→Settings(macOS 是PyCharm→Preferences) - 进入
Project: 项目名→Python Interpreter - 如果项目里已经有
.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语言,如何实现后端功能呢?要实现一个完整的后端服务,我们需要从两个层面来考虑:
- Web服务器层面:解决”如何接收请求”的问题
- 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的瘦身版,它的设计理念完全不同:
| 特性 | Tomcat | Uvicorn |
|---|---|---|
| 部署应用数量 | 支持多个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!', # 响应内容(必须是字节类型)
})
```
代码解析:
async def app(...): 定义异步函数,函数名可以任意,但参数必须是scope, receive, sendawait send(...):await关键字表示”等待”这个异步操作完成- 响应分两步发送:
- 第一步:发送响应状态码和响应头
- 第二步:发送响应体内容
一个常见疑问:响应头和响应体能不能合成一条 send 一起发?
不能。在 ASGI(Uvicorn 所遵循的协议)里,响应头和响应体必须分成两条消息发送,这是协议的硬性规定——不是写法上的可选项:
type为http.response.start的消息:只携带状态码(status)和响应头(headers),不能带响应体type为http.response.body的消息:只携带响应体(body),不能带状态码和响应头
为什么要这样拆开? 有两个原因:
- HTTP 响应本身就是”先头后体”的顺序结构:一条 HTTP 响应报文,永远是先有响应行+响应头,然后才是响应体。ASGI 用两条消息正好对应这个先后顺序。
- 为了支持流式响应:响应体可以分很多次发送——比如返回一个大文件、或者像后面会讲的 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. ```
第二种方式
- 创建一个新的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 # 开启热重载
)
```
- 在PyCharm中运行
run_server.py
不管通过哪种方式,只要启动成功后打开浏览器访问:
``` http://127.0.0.1:8000 ```
你应该能看到页面显示:
``` Hello from Uvicorn! ```
—
Web应用层面
通过上一节的学习,我们已经知道要使用Uvicorn服务器,就必须自己定义一个异步函数真正处理所接收的请求
那么问题来了,如果要让我们自己真的来实现这样一个函数,我们需要在这个函数中实现什么功能?
让我们一起分析一下,至少有以下四点需要实现:
- 区分不同的请求: 一个Web应用所要处理的请求肯定不止一个,比如用户访问
/login和/register应该执行不同的逻辑,我们需要根据请求路径、请求方法(GET/POST)来区分请求 - 解析请求参数: 请求中可能携带路径参数,可能携带key-value形式的查询参数,也可能携带请求体参数(如POST请求的JSON数据),我们需要从请求中解析这些参数
- 实现业务逻辑: 根据不同的请求,调用不同的业务处理代码,可能需要查询数据库、调用AI接口、处理文件等
- 封装响应:将处理结果转换为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方法 | SpringMVC | FastAPI |
|---|---|---|
| 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(比如 UserController、OrderController)。FastAPI 也提供了类似的机制:APIRouter。
APIRouter 可以理解为一个”子路由器”——先把一组相关的接口注册到这个子路由器上,最后再统一挂载到主 app。它的对应关系是:
| FastAPI | SpringMVC | 作用 |
|---|---|---|
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开发中,请求参数主要有三种形式:
- 路径参数(Path Parameter):作为URL路径的一部分
- 查询参数(Query Parameter):URL中
?后面的key-value形式参数 - 请求体参数(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
```
两者长得几乎一样,但有一个关键区别——是否做数据校验。
先看两者作为普通数据类自身的能力对比:
| @dataclass | Pydantic 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
```
代码解析:
- 定义数据模型:
class UserRegister(BaseModel): 继承自Pydantic的BaseModel- 类中的每个属性都有类型注解
- 可以设置默认值(如
email: str = "")
- 接收请求体:
def register(user: UserRegister): 函数参数类型为我们定义的模型- FastAPI会自动:
- 读取请求体中的JSON数据
- 验证数据类型是否正确
- 将JSON转换为UserRegister对象
- 如果验证失败,自动返回400错误和详细的错误信息
- 访问数据:
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]
}
```