本文档详细介绍了 python 泛型的用法,并通过大量代码示例展示如何在 python 中使用泛型进行类型安全编程。同时,我们也会对比 java 的泛型机制,帮助你更好地理解两者的区别。
1. 为什么需要泛型?
python 是一门动态语言,但在大型项目中,为了提高代码的可维护性和减少 bug,我们通常会使用类型提示 (type hints)。
泛型允许我们在定义函数、类或接口时,不指定具体的数据类型,而是在使用时再指定。
主要好处:
- 类型安全:静态类型检查器(如
mypy)可以在运行前发现类型错误。 - 代码复用:一套逻辑可以应用于多种数据类型。
- ide 智能提示:更好的自动补全和代码导航。
2. 基础概念与语法
2.1 定义类型变量 (typevar)
在 python 中(3.12 之前),泛型的核心是 typevar。
必须先定义一个类型变量对象,才能在后续代码中使用它。
from typing import typevar, list
# 定义一个类型变量 t
# 习惯上变量名和字符串参数保持一致
t = typevar('t')
2.2 泛型函数
一个简单的例子:实现一个函数,返回列表中的第一个元素。
from typing import typevar, list
t = typevar('t')
def get_first(items: list[t]) -> t:
"""返回列表的第一个元素,类型与列表元素类型一致"""
return items[0]
# 使用示例
n: int = get_first([1, 2, 3]) # t 被推断为 int
s: str = get_first(["a", "b"]) # t 被推断为 str
# ide 会报错的例子:
# x: str = get_first([1, 2, 3]) # 错误: 期望返回 int,但标记为 str
2.3 泛型类
使用 generic[t] 基类来定义泛型类。
from typing import typevar, generic
t = typevar('t')
class stack(generic[t]):
def __init__(self) -> none:
self.items: list[t] = []
def push(self, item: t) -> none:
self.items.append(item)
def pop(self) -> t:
return self.items.pop()
# 具体化使用
int_stack = stack[int]()
int_stack.push(1)
# int_stack.push("a") # 类型检查错误: 期望 int
str_stack = stack[str]()
str_stack.push("hello")
2.4 多个类型变量
类似于 java 的 map<k, v>。
k = typevar('k')
v = typevar('v')
class keyvaluepair(generic[k, v]):
def __init__(self, key: k, value: v):
self.key = key
self.value = value
pair = keyvaluepair[str, int]("age", 25)
2.5 上界约束 (bound)
有时我们需要限制 t 必须是某个类的子类。
class animal:
def speak(self): pass
class dog(animal): ...
class cat(animal): ...
# t 必须是 animal 或其子类
a = typevar('a', bound=animal)
def make_noise(animal: a) -> none:
animal.speak()
make_noise(dog()) # ok
# make_noise("hello") # error: str 不是 animal 的子类
3. python vs java 泛型对比
这是最关键的部分,理解两者的差异有助于你从 java 思维转换到 python 思维。
3.1 语法对比
| 特性 | java | python (3.5 - 3.11) | python (3.12+) |
|---|---|---|---|
| 定义泛型类 | class box<t> { ... } | class box(generic[t]): ... | class box[t]: ... |
| 定义泛型方法 | public <t> t func(t x) | def func(x: t) -> t: | def func[t](x: t) -> t: |
| 类型变量声明 | 隐式声明 (直接写 <t>) | 必须显式声明 (t = typevar('t')) | 隐式声明 (3.12+ 新语法) |
| 实例化 | new box<integer>() | box[int]() | box[int]() |
| 通配符 | list<?> | list[any] | list[any] |
| 上界约束 | <t extends number> | typevar('t', bound=number) | class box[t: number]: |
3.2 核心机制差异
java: 伪泛型与类型擦除 (type erasure)
- 机制:java 编译器在编译时检查类型,但在生成的字节码中,所有的
t都会被替换成object(或其他上界)。运行时 jvm 不知道list<string>和list<integer>的区别。 - 后果:你不能在运行时做
if (obj instanceof t)这样的检查。
python: 运行时对象与静态检查
- 机制:python 是动态的。
generic[t]和typevar('t')都是运行时的真实对象。 - 检查:python 解释器本身完全忽略这些类型提示,不会在运行时报错(除非代码逻辑本身错了)。类型检查完全依赖外部工具(如
mypy,pyright, 或 ide)。 - 后果:你可以运行
x: int = "hello",python 解释器照样执行不误。必须配合mypy使用才有意义。
3.3 代码直接对比
java:
// java 不需要提前定义 t
public class box<t> {
private t content;
public void set(t content) {
this.content = content;
}
public t get() {
return content;
}
}
// 使用
box<string> box = new box<>();
box.set("hello");
python:
from typing import typevar, generic
# python 必须先定义 t
t = typevar('t')
class box(generic[t]):
def __init__(self) -> none:
self.content: t = none
def set(self, content: t) -> none:
self.content = content
def get(self) -> t:
return self.content
# 使用
box = box[str]()
box.set("hello")
3.4 上界约束对比 (upper bound)
java 使用 extends 关键字来实现上界约束,而 python 在 typevar 定义中使用 bound 参数。
java:
// t 必须是 animal 或其子类
public class zoo<t extends animal> {
private t animal;
public void set(t animal) {
// 可以安全调用 animal 的方法
animal.speak();
}
}
python:
# t 必须是 animal 或其子类
t = typevar('t', bound='animal')
class zoo(generic[t]):
def __init__(self, animal: t):
self.animal = animal
def set(self, animal: t) -> none:
# 可以安全调用 animal 的方法
self.animal.speak()
4. 进阶用法示例 (结合你的项目)
在 rag 系统或数据处理管道中,泛型非常有用。
4.1 泛型 repository 模式
from typing import typevar, generic, list, optional
from dataclasses import dataclass
# 假设有两个实体模型
@dataclass
class user:
id: int
name: str
@dataclass
class document:
id: int
content: str
# 定义泛型 t,约束为必须有 id 属性 (这里用 protocol 更高级,但简化演示用)
t = typevar('t')
class baserepository(generic[t]):
def __init__(self):
self.db: dict[int, t] = {}
def save(self, entity: t) -> none:
# 假设实体都有 id 属性
self.db[entity.id] = entity
def get(self, id: int) -> optional[t]:
return self.db.get(id)
def find_all(self) -> list[t]:
return list(self.db.values())
# 具体实现
class userrepository(baserepository[user]):
def find_by_name(self, name: str) -> optional[user]:
for user in self.db.values():
if user.name == name:
return user
return none
# 使用
user_repo = userrepository()
user_repo.save(user(1, "alice"))
user = user_repo.get(1) # 类型自动推断为 user
4.2 泛型 protocol (类似 java interface)
如果你想定义一个“只要有 read() 方法的对象”,不管它继承自谁。
from typing import protocol, typevar
t = typevar('t')
class reader(protocol[t]):
def read(self) -> t:
...
def process_data(reader: reader[str]) -> none:
print(reader.read())
class filereader:
def read(self) -> str:
return "file content"
# filereader 没有继承 reader,但符合结构,可以通过检查
process_data(filereader())
5. 总结
- 显式定义:python (3.12前) 需要
t = typevar('t')。 - 继承 generic:类需要继承
generic[t]才能成为泛型类。 - 工具检查:泛型主要服务于静态检查工具和 ide,运行时不会强制校验。
- 灵活性:python 的泛型系统非常强大,配合
protocol(结构化类型) 可以实现比 java 更灵活的模式。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论