Python 泛型是如何被“擦除”的?

泛型擦除发生在Python导入模块或执行字节码之前,由解释器在解析类型注解(__annotations__)时完成;源码中如list[str]在运行时变为list,下标信息丢失。

Python 的泛型在运行时会被完全擦除,只在类型检查阶段起作用。这是因为 Python 采用的是“类型提示”(type hints)机制,而非真正的静态类型系统。泛型参数(如 List[str]Dict[int, bool] 或自定义的 Stack[T])在代码执行前就被丢弃,解释器看到的只是原始类型(如 listdictStack)。

泛型擦除发生在哪个环节?

擦除发生在 Python 导入模块或执行字节码之前,由解释器在解析类型注解(__annotations__)时完成。具体来说:

  • 源码中的 def foo(x: list[str]) -> dict[str, int]: ... 在运行时,x 的注解变成 list,返回注解变成 dict
  • typing.List[str](旧写法)和 list[str](PEP 585 写法)都会被归一化为 list,下标信息丢失;
  • 通过 get_type_hints() 获取的也是擦除后的类型,除非显式传入 include_extras=True 并配合 typing.get_args() 等工具尝试还原(但无法恢复完整泛型结构)。

为什么 Python 要擦除泛型?

设计上为了兼容动态性与运行时灵活性:

  • 不改变对象的实际行为:一个 list 不会因为标注为 list[str] 就拒绝添加整数;
  • 避免运行时开销:无需在每次访问元素时检查类型,也不需为每种泛型组合生成新类;
  • 保持与已有代码无缝衔接:所有类型提示都是可选的、非强制的,擦除后不影响任何逻辑执行。

擦除带来的实际影响

开发者需注意以下常见情况:

  • 无法在运行时做泛型参数判断:比如不能写 if T is str:,因为 TTypeVar 对象,且其绑定信息不保留;
  • 序列化/反射受限:用 dataclassespydantic 时,字段类型若含泛型(如 items: list[UUID]),框架需靠额外机制(如字符串解析、AST 预处理)推断参数,而非直接读取运行时类型;
  • 自定义容器需手动处理:如果实现类似 Box[T] 的类,要支持类型感知(如 Box[int].__args__),必须依赖 typing.get_args() 解析原始注解字符串或 AST,不能靠实例属性直接获取。

有没有绕过擦除的方法?

严格来说没有“绕过”,但有折中方案:

  • 使用 typing.get_origin()typing.get_args() 从注解对象中提取泛型结构(仅限静态可用的注解,且要求未被 eval 过度简化);
  • 在类定义中显式保存参数,例如:class Stack(Generic[T]): def __init__(self, item_type: type[T]): self.item_type = item_type
  • 借助第三方库如 typing-inspecttyping_extensions 增强对泛型元数据的访问能力,但仍受限于擦除前提。