Python 泛型:把 listUser 讲明白
一篇把 Python 泛型讲透的拆解文。
list[User]到底在干嘛?TypeVar是啥?怎么自己写一个泛型类?
前言
FastAPI 文档里"泛型"这一段,写得克制,但经常把人卡住。
不是因为它难------而是因为网上讲泛型的文章一上来就甩 TypeVar、Generic[T],没人先解释这个机制到底在解决什么问题。
读完你应该能:知道 list[User] 在干嘛、能自己写一个泛型类、能在 FastAPI 里把它用起来。
一、泛型是啥?
先看 FastAPI 文档原文:
某些类型可以在方括号内接收"类型参数",以定义其内部类型。例如,"字符串列表"可以声明为
list[str]。这些可以接收类型参数的类型被称为泛型。
一个常见反应:list[str] 就是 str 列表,这还用专门讲?
用生活例子理解:
装饼干的盒子 →
Box[饼干]装袜子的盒子 →Box[袜子]
Box[...] 就是泛型 ,方括号里那个就是类型参数。
看代码:
python
# 一开始你只知道是"列表",但里面是什么类型?
names: list # ❌ 不知道装啥
# 加上方括号 → "字符串列表"
names: list[str] # ✅ 明确:里面都是 str
# 字典也一样:键是 str,值是 int
scores: dict[str, int] # {"math": 90, "english": 85}
简单说:把类型当参数,让同一份代码适配多种类型。
二、list[User] 都指定死了,还"通用"吗?
都指定成 User 了,还"通用"在哪儿?
澄清一下:泛型指的是那个能接收类型参数的模板,不是你这个具体类怎么用。
list是泛型(它能接收类型参数)list[User]是把泛型实例化 了,里面的User是具体的类型参数
只要是 xxx[类型] 这种形式,xxx 就是泛型 ------不管你里面写的是 User 还是 T 还是 int。
那"通用"到底通用在哪?
通用的是 list 这个类本身的实现------不是你这次怎么用。
如果 list 只能装一种类型会怎样?Python 的 C 实现(CPython)里需要这么写:
python
class StringList: ... # 装字符串的列表
class IntList: ... # 装整数的列表
class UserList: ... # 装 User 的列表
# ... 永远写不完
但实际上,CPython 里的 list 只有一份 实现(Objects/listobject.c):
c
// 简化版 CPython list 的 append
int list_append(PyListObject *self, PyObject *item) {
// item 是 PyObject* 指针,指向任何 Python 对象
// 不管 item 实际是 str、int 还是 User,都接受
...
}
这份 C 代码不挑类型------你 list.append(str)、list.append(int)、list.append(User),跑的是同一份 C 代码。
所以"通用"不在你怎么用,在 list 这个类本身。 你写 list[User] 还是 list[Order],运行时都跑同一份 CPython 源码。
三、Python 运行时泛型是装饰品
Python 运行时,方括号里的东西是装饰品。
python
a: list[User] = [1, 2, 3] # 运行时不会报错
b: list = [1, 2, 3]
c = [1, 2, 3]
运行时 Python 完全不挑------你往里塞啥都行。
那它有没有用?------有用,但只给外部工具用。
1. 类型检查器(mypy)
用 uv 装 mypy:
bash
uv tool install mypy
写 script.py:
python
class User:
name: str
age: int
def get_users() -> list[User]:
return []
users = get_users()
print(users[0].name) # OK
print(users[0].xxx) # ❌ 错!User 没有 xxx
跑 mypy:
bash
$ mypy script.py
script.py:10: error: "User" has no attribute "xxx" [attr-defined]
Found 1 error in 1 file (checked 1 source file)
xxx 不存在被 mypy 抓出来了------这就是方括号的作用。
2. FastAPI / Pydantic
实际工程里,给接口配 response_model=APIResponse[User] 之后:
python
@app.post("/lark-tools", response_model=APIResponse[User])
def create_user():
return {"code": 0, "msg": "ok", "data": {"name": "mavis"}} # 少一个 age
触发接口后,server 端会抛 ResponseValidationError:
css
fastapi.exceptions.ResponseValidationError: 1 validation error:
{'type': 'missing',
'loc': ('response', 'data', 'age'),
'msg': 'Field required',
'input': {'name': 'mavis'}}
Pydantic 看到 APIResponse[User],校验出 data 缺 age 字段------泛型不是装饰品,框架在用方括号做真正的运行时校验。
两个层面
| 层面 | 方括号起作用吗 | 怎么起作用 |
|---|---|---|
| 纯 Python 解释器 | ❌ | 方括号被忽略 |
| 类型检查器(mypy) | ✅ | 静态分析、代码补全 |
| FastAPI / Pydantic | ✅ | 运行时校验、生成文档 |
所以 Python 借了泛型的语法,但运行时解释器不认它------认它的是类型检查器和框架。
四、TypeVar 与 Generic[T]:自己定义一个泛型类
TypeVar("T") 是啥
python
from typing import TypeVar
T = TypeVar("T")
TypeVar("T") 创建一个叫"T"的类型变量。这里的 "T" 是个名字(类似变量名),用在错误信息里------T 这个名字在代码里就是约定的占位符,你可以改成 MyType、K 啥的,但习惯上用单个大写字母(T、K、V)。
python
T = TypeVar("MyType") # 也行,错误信息里会显示 "MyType"
T 不是"一种类型",是"任何类型的占位符" 。你用 T 的地方,最后都会被推断成具体的某个类型。
Generic[T] 是啥
Generic[T] 让类支持类型参数。没有 Generic[T],你不能在类里用 T 这个类型变量------会直接报错。
python
from typing import Generic, TypeVar
T = TypeVar("T")
class Box(Generic[T]): # 声明这个类支持类型参数 T
def __init__(self, item: T):
self.item = item
def get(self) -> T: # 标返回类型也是 T
return self.item
TypeVar 的作用:让类型"流"过去
装 mypy 后,在 script.py 里写:
python
from typing import Generic, TypeVar
T = TypeVar("T")
class Box(Generic[T]):
def __init__(self, item: T):
self.item = item
def get(self) -> T:
return self.item
# T 推断为 str
b1 = Box("hello")
reveal_type(b1.get()) # mypy: Revealed type is "builtins.str"
# T 推断为 int
b2 = Box(42)
reveal_type(b2.get()) # mypy: Revealed type is "builtins.int"
b1 装的是 str,b1.get() 出来就是 str;b2 装的是 int,b2.get() 出来就是 int。入参是啥类型,出参就是啥类型 ------这就是 TypeVar 干的事。
如果用 Any 代替,就没这个效果:
python
from typing import Any
class BadBox:
def __init__(self, item: Any):
self.item = item
def get(self) -> Any:
return self.item
b = BadBox("hello")
reveal_type(b.get()) # mypy: Revealed type is "Any"
Any 啥都行,但啥都不保证。
五、FastAPI 实战:通用响应包装
实战代码:
python
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Generic, TypeVar
app = FastAPI()
T = TypeVar("T")
class APIResponse(BaseModel, Generic[T]):
code: int
msg: str
data: T
class User(BaseModel):
name: str
age: int
class Order(BaseModel):
id: str
total: float
@app.get("/user", response_model=APIResponse[User])
def get_user():
return {"code": 0, "msg": "ok", "data": {"name": "mavis", "age": 18}}
@app.get("/order", response_model=APIResponse[Order])
def get_order():
return {"code": 0, "msg": "ok", "data": {"id": "o001", "total": 99.0}}
跑起来:
bash
$ uv run uvicorn main:app --reload
/user 返回的 data 按 User 校验,/order 返回的 data 按 Order 校验------一份 APIResponse[T] 代码,撑起所有返回结构。这就是泛型在工程里的价值。
总结
- 本质:泛型 = 把类型当参数,让一份代码适配多种类型。
- 运行时:解释器不认方括号,但 mypy / Pydantic 认。
- 用法 :要写"装任意类型的容器"时------
TypeVar占位 +Generic[T]让类认它。