Python 的 list[int] 为什么不是 list?拆开 __class_getitem__ 和那层 80 字节的薄包装
先看几行我在本机跑出来的结果(CPython 3.13.14):
>>> type(list[int])
<class 'types.GenericAlias'>
>>> list[int] is list[int]
False
>>> list[int] == list[int]
True
>>> list[int]((1, 2, 3))
[1, 2, 3]
>>> list[int](['a', 'b'])
['a', 'b'] # 说好的 int 呢?
list[int] 不是 list,不是字符串,也不是类型注解专用的语法糖。它是一个实实在在的对象,占 80 字节,可以被调用,而且调用时完全不检查 int。
很多人写过几百遍 list[int],从没拆过它到底是什么。这篇就拆这一层。
一、它是 types.GenericAlias,不是 list
import types, sys
g = list[int]
print(isinstance(g, types.GenericAlias)) # True
print(g.__origin__) # <class 'list'>
print(g.__args__) # (<class 'int'>,)
print(sys.getsizeof(g)) # 80
print(str(g)) # 'list[int]'
结构很简单:一个 __origin__ 指向原始容器,一个 __args__ 存着方括号里的东西,再加几个标准方法。80 字节,就是这么个薄包装。
它是每次求值都新建一个对象:
>>> list[int] is list[int]
False
>>> list[int] == list[int]
True
== 按 __origin__ 和 __args__ 比,所以相等;is 比身份,所以不等。别拿 is 去判断两个泛型别名是否相同。
二、它居然能被调用,且不检查类型
>>> callable(list[int])
True
>>> list[int]((1, 2, 3))
[1, 2, 3]
>>> list[int](['a', 'b'])
['a', 'b']
GenericAlias.__call__ 直接把参数转交给 __origin__,也就是 list(...)。方括号里的 int 在这一步完全不参与,纯属贴在包装上的标签。
这不难理解:类型注解在运行时默认是零成本的。如果 list[int](...) 真去逐个校验元素,那每次构造列表都要多一遍遍历,标准库不会这么设计。类型检查是 mypy / pyright 这些静态工具的事,不是运行时的活。
一个容易踩的对比,来自 typing 模块:
import typing
tg = typing.List[int]
print(type(tg).__name__) # _GenericAlias
print(callable(tg)) # True
tg((1, 2)) # TypeError: Type List cannot be instantiated; use list() instead
typing.List[int] 是另一套实现(_GenericAlias),明确禁止实例化;内置的 list[int] 允许。写业务代码时统一用内置的 list[int],typing.List 是 Python 3.9 之前的老写法。
三、为什么自定义类写 C[int] 会报错
class C:
pass
>>> C[int]
TypeError: type 'C' is not subscriptable
方括号这个动作,走的是 __getitem__ 协议;对类对象来说,特殊入口叫 __class_getitem__。内置容器(list / dict / tuple / set / type 等)在 C 层面实现了它,普通类没有,所以报错。
自己加一个就行,返回值随便你定:
class D:
def __class_getitem__(cls, item):
return ('D_GENERIC', item)
>>> D[int]
('D_GENERIC', <class 'int'>)
注意:__class_getitem__ 不要求返回 GenericAlias,它只是个钩子。标准库里 list[int] 返回 types.GenericAlias,但你自己完全可以返回字符串、元组,甚至直接返回 cls。
顺带一个细节:
>>> hasattr(list[int], '__mro_entries__')
True
__mro_entries__ 是给「把泛型别名当基类」用的:class MyList(list[int]): ... 时,Python 会靠它把 list[int] 还原成 list 再建类。这也是 PEP 560 那套机制的一部分。
四、性能账:11.7 倍
import timeit
# 50 万次取平均
list[int] -> 178.2 ns/次
list -> 15.2 ns/次
本机实测 list[int] 比光秃秃一个 list 名字慢 11.7 倍。原因也清楚:前者要构造一个 80 字节的对象、填 __origin__ 和 __args__,后者只是从名字空间里读一个已存在的全局。
所以我自己遵守两条:
- 注解写在函数签名里,别写在热循环里。 函数定义时求值一次,之后不再碰;循环里每轮
list[int]就是每轮新建对象。 from __future__ import annotations(或 3.14 起的默认延迟求值)能把注解变成字符串,连这一次构造都省了——需要内省时用typing.get_type_hints()再还原。
顺带说清楚一个常见误会:list[int] 慢,不代表「类型注解让 Python 变慢」。慢的是运行时真的去构造这个别名对象这件事,而正常注解只在定义时发生一次。
五、它到底解决什么问题
说白了,list[int] 的存在感几乎全在给工具看:
- 静态检查器靠
__origin__/__args__知道这是个「元素为 int 的列表」; - IDE 靠它补全;
dataclasses、pydantic、FastAPI这类库在运行时用get_type_hints()读它,决定怎么校验、怎么序列化;- 对你自己写的
for x in data:,它什么都不做。
一个 80 字节的标签,把「人类和工具需要的信息」挂在类型上,又不给运行时添麻烦——这是 Python 处理类型提示的典型思路:信息要留着,但别在跑的时候付账。
实验脚本在 d51-probe.py,数值随机器和版本会变,但「每次新建、可调用、不检查、比裸名字慢一个数量级」这几条是稳定的。

浙公网安备 33010602011771号