Python 3.12的TypeVar语法变迁:PEP 695让泛型写起来像呼吸一样自然

2026-08-04 0 728

我先说个真实感受:以前看到 Python 里的泛型,心里总觉得“能用,但不太优雅”。你要定义一个泛型函数,必须写 T = TypeVar("T"),然后函数里再带上 def first(seq: list[T]) -> T,看起来还行,但一旦需要绑定类型、约束泛型或者定义变长参数,代码马上变得啰嗦。

好在前段时间升级到 Python 3.12,发现之前很多“反人类”的类型语法都被换掉了。新出的 PEP 695 把泛型定义直接放进了函数和类的声明里,不用再单独声明 TypeVar,代码清晰了不止一个档次。这篇文章不扯别的,就用实际案例把 PEP 695 的核心变化掰开揉碎。

1. 为什么 3.12 之前的泛型写起来让人头疼

看这个老式写法,定义一个返回列表第一个元素的函数:

from typing import TypeVar

T = TypeVar("T")

def first_element(values: list[T]) -> T:
    return values[0]

这还行。但如果你的泛型需要满足一个协议,比如有 .price 属性,那你得写:

class Product:
    price: float

P = TypeVar("P", bound=Product)

def get_total(products: list[P]) -> float:
    return sum(p.price for p in products)

除了多写一个 TypeVar 变量,还必须在类和函数之间传递,导致你经常要往上翻找。而且 TypeVar 的 bound、covariant 这些参数很容易被小白误用。

老版本里还有一种更让人困惑的写法:你要在多个函数里复用同一个类型变量,就得把它定义成模块级变量,否则会在类型提示里变红。

PEP 695 的初衷,就是让类型参数直接“内联”到函数签名或类名后,不再单独声明。这本质上是一种语法糖,但糖得很舒服。

2. 新的类型参数语法:用 [ ] 取代 TypeVar

PEP 695 允许你在函数名后面直接跟方括号,里面写类型参数的名字,就像这样:

def first_element[T](values: list[T]) -> T:
    return values[0]

解释一下:def first_element[T](...) 等同于定义了一个 TypeVar 叫 T,然后范围就是这个函数体。你不需要再单独写 T = TypeVar("T"),类型检查器也会把 T 自动当作泛型变量。

对类来说也一样:

class Box[T]:
    def __init__(self, item: T):
        self.item = item

    def get(self) -> T:
        return self.item

实例化的时候直接使用具体类型:

box = Box[int](42)
print(box.get())  # 42

这种写法显然更干净。最关键的是,函数和类的泛型现在可以定义在作用域内部,不会污染模块命名空间,也不怕不同模块里的同名 T 冲突了。

3. 处理多个类型参数的场景

以前多类型参数要定义好几个 TypeVar,现在直接在方括号里用逗号隔开:

def zip_map[K, V](keys: list[K], values: list[V]) -> dict[K, V]:
    return dict(zip(keys, values))

你再也不用写下边这样啰嗦的代码:

K = TypeVar("K")
V = TypeVar("V")
def zip_map(keys: list[K], values: list[V]) -> dict[K, V]:
    return dict(zip(keys, values))

真的,就一个语法变化,代码量减少一小半,心智负担也降低不少。

4. 类型参数的约束:用冒号代替 TypeVar bound

如果你希望泛型 T 必须是某个特定类的子类,新语法直接在方括号里写:

from decimal import Decimal

def total[P: Product](products: list[P]) -> float:
    return sum(p.price for p in products)

等价于老写法:

P = TypeVar("P", bound=Product)
def total(products: list[P]) -> float:
    return sum(p.price for p in products)

可以看到,新写法把 bound 信息直接放在了参数名后面,语义更加直观。

另外,你还可以用 tuple[T, ...] 这种新的语法表示变长元组,PEP 695 也支持了。比如:

def vector[T](*args: T) -> tuple[T, ...]:
    return args

这在旧版里要这样做:

T = TypeVar("T")
def vector(*args: T) -> tuple[T, ...]:
    return args

顺便提一嘴,新语法还支持 *Ts 这种可变类型参数(TypeVarTuple)的简写,但普通开发中基本用不到,这里先不展开。

5. 泛型类之间的继承

在继承时使用泛型,新语法也顺手了不少。以前定义一个非泛型的子类,你需要写上泛型参数,比如:

T = TypeVar("T")

class Base(Generic[T]):
    def method(self) -> T: ...

class IntBox(Base[int]):
    pass

现在呢?

class Base[T]:
    def method(self) -> T: ...

class IntBox(Base[int]):
    pass

基类的泛型参数直接写进方括号,不用再 import Generic 了。对使用者来说,都是一样的。

如果你希望子类也保留泛型,可以这样:

class Base[T]:
    pass

class Sub[T](Base[T]):
    pass

是不是有点类似 TypeScript 的那种泛型写法?确实,Python 这次借鉴了很多静态语言的风格,反而让代码更容易读了。

6. 泛型类型别名也变了

老版本中,定义一个类型别名常常要这样玩:

from typing import TypeAlias

DictStrInt = dict[str, int]

PEP 695 虽然没改变类型别名的常规用法,但结合新的泛型语法,你可以这样定义泛型别名:

type Matrix[T] = list[list[T]]

然后直接使用:

def print_matrix(m: Matrix[int]) -> None:
    for row in m:
        print(row)

这里的 type 关键字是 Python 3.12 引入的,专门用来声明类型别名。相比之前用 TypeAlias 再配合赋值,现在看起来更像定义类型本身。

还有一个小细节:泛型别名的类型参数也可以有约束:

type Tagged[T: str] = tuple[T, int]

不过实际上 str 已经是不可变类型,这种约束意义不大,只是展示语法。

7. 实战用例:写一个简单的 Repository 基类

光看语法片段不过瘾,我拿平时做后端常用的 Repository 模式来举个例子。

假设我们有一个 ORM 模型基类,想要定义一个通用的仓储基类,里面用泛型约束模型类型:

from typing import Iterator

class Model:
    id: int

class User(Model):
    name: str

class Product(Model):
    price: float

class Repository[M: Model]:
    def __init__(self, data: dict[int, M]):
        self._data = data

    def get_by_id(self, model_id: int) -> M:
        return self._data[model_id]

    def list_all(self) -> Iterator[M]:
        return iter(self._data.values())

这以前要写多少 TypeVar?现在直接泛型参数 M 继承自 Model,简洁明了。实际使用:

user_repo = Repository[int, User]?  # 不对,我们只用了 M 一个参数,但 dict[int, M] 的类型是明确的
user_repo = Repository[User]({1: User(id=1, name="Tony")})
print(user_repo.get_by_id(1).name)

仔细看 class Repository[M: Model],就是定义了一个类型参数 M,并约束 M 必须是 Model 的子类。内部 dict[int, M] 表示 key 是 int 类型,value 是 M 类型。很棒。

8. 与 Pydantic、FastAPI 结合的感受

很多小伙伴日常用 FastAPI 做 API,里面大量使用类型提示。PEP 695 虽然目前主要靠静态扫描,但 Pydantic 和 FastAPI 已经在做适配。

比如你定义一个泛型返回值:

class ApiResponse[T]:
    data: T
    message: str

在 FastAPI 里返回这个类时,如果用了 3.12 的新语法,类型检查器能更准确地推断出 data 字段的实际类型。这在编译期就能发现 bug,而不是等到运行时。

不过要注意,目前 Pydantic v2 对 PEP 695 的支持还有些坑。你在写 class Box[T]: 时,Pydantic 可能不能自动解析内部的泛型模型。官方表示在推进,但目前建议使用 BaseModel 时还是用旧方式。如果你不是写库,只是业务代码,影响不大。

9. 几个容易踩的坑

新语法刚上手,总有地方会卡壳。我列几个自己遇到过的:

  • 不要在类名后的方括号里用引号。老版本 TypeVar 有字符串前向引用,但新语法是真实的对象,不能写 class Foo["Bar"],需要直接写 class Foo[Bar],如果 Bar 还没定义,可以用 if TYPE_CHECKING 规避。
  • 类型参数的作用域仅在当前函数/类内部,如果在方法里需要使用外层的泛型,请直接用方法参数里的泛型类,比如 self 的类型是 Self,不容易混淆。
  • 新语法目前需要 Python 3.12 以上解释器,并且 Pyright/Pycharm 最新版才支持。VSCode 的 Pylance 已经适配,旧版 PyCharm 可能显示为错误。
  • 如果你的项目里大量使用 `from typing import TypeVar`, 升级后不删也没关系,新旧可以混用。但最好统一使用新语法,免得代码风格不一。

10. 总结:新语法到底值得换吗

在我看来,PEP 695 不是那种“必须要立刻用起来”的更新,但它确实让代码的“类型感”更强烈了。以前很多人不喜欢写类型提示,就是觉得 TypeVar 麻烦;现在语法简单了,大家自然更愿意写泛型。

当你写一个通用工具库的时候,这种改进尤其明显。它的意义不只是少写两行代码,而是让你思考泛型的方式更直接——类型就是参数的一部分,像函数参数一样放在那里,不用绕弯子。

但如果你还在用 3.10 或更低版本,也不要急着升,毕竟升级不是小事。先把新语法在单独的环境里跑一跑,感受一下,等项目慢慢迁移。未来三年内,3.12 会成为主流,那时候再换完全来得及。

反正我现在已经把所有新写的代码都用上了 PEP 695。Python 的类型系统在慢慢进化,这是好事。就像当年从 Python 2 到 Python 3 一样,虽然还有阵痛,但往前看,方向总是好的。

Python 3.12的TypeVar语法变迁:PEP 695让泛型写起来像呼吸一样自然
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 python Python 3.12的TypeVar语法变迁:PEP 695让泛型写起来像呼吸一样自然 https://www.taomawang.com/server/python/2487.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务