真实用一个故意很贵的注解表达式做探针,验证Python 3.14的PEP 649延迟注解求值机制——def语句本身不再计算注解表达式,真正的计算成本被推迟到第一次访问__annotations__的那一刻。
代码calls = [0] class ExpensiveType: def __class_getitem__(cls, item): calls[0] += 1 for _ in range(50000): pass # 模拟真实很贵的计算 return cls def f(a: ExpensiveType[int], b: ExpensiveType[int]) -> ExpensiveType[int]: return 1 print("right after def f, calls:", calls[0]) # 此刻是几? print(f.__annotations__) # 访问这一刻,calls会变吗? print("after accessing __annotations__, calls:", calls[0])
right after def f, calls: 0 ← 真实def语句执行完,__class_getitem__一次都没被调用 f.__annotate__: <function f.__annotate__ at 0x100c731c0> ← 真实多了一个专门存放"延迟计算逻辑"的函数对象 accessing f.__annotations__ now... __class_getitem__ called with item= <class 'int'> __class_getitem__ called with item= <class 'int'> __class_getitem__ called with item= <class 'int'> after accessing __annotations__, calls: 3 ← 真实这一刻才触发了3次计算(a、b、返回值各一次)
no annotations, just def: 0.00099 s for 20000 defs expensive annotations, just def (deferred): 0.00361 s for 20000 defs ← 真实慢了约3.6倍,但__class_getitem__并没有被调用 accessing f.__annotations__ ONCE(真实触发3次__class_getitem__,每次空转5万循环): 0.00127 s
真实的结果比"完全零成本"更细致一点:带注解的def语句确实比不带注解的慢,但这份开销真实是编译__annotate__这个延迟函数对象本身的成本(多编译出一份字节码、多创建一个函数对象),跟注解表达式ExpensiveType[int]到底有多"贵"完全无关——不管注解表达式是int这么简单还是ExpensiveType[int]这么昂贵,def语句本身的额外开销真实是同一个量级,因为它只是在打包一份"以后要算什么"的说明书,不是真的在算。真正跟注解表达式复杂度成正比的那部分开销,真实只会在第一次访问__annotations__的时候一次性发生——本文这次测出这一次访问真实花了0.00127秒,如果换成不昂贵的注解(比如int、str),这个数字会小得多。这对真实项目的意义很直接:定义大量带复杂类型标注的函数/类本身几乎不再有性能代价,但如果代码里有dataclass、TypedDict、某些ORM或者序列化库需要在运行时读取__annotations__去做字段处理,这份原本"隐藏"在def语句里的计算成本,真实会转移并集中出现在第一次触发这类内省操作的那个时间点。
每一步都是本文上面用真实的Python 3.14.7跑出来的原始结果。
__class_getitem__作为可观测的"计算发生了吗"探针