《Python高级编程》8.3 注解的求值时机(PEP 649/749)

Python 3.14 的 PEP 649/749 让注解不再在函数、类、模块定义时求值,而是延迟到访问 __annotations__ 时才求值。本节实测 __annotate__、annotationlib 的三种 Format、缓存行为、前向引用与循环引用,以及 from __future__ import annotations 的关系。

本节目标:说清 Python 3.14 的注解为什么、以及如何「延迟求值」——__annotate__ 是什么、annotationlib 的三种 Format 各返回什么、前向引用与循环引用怎么解决,并实测 from __future__ import annotations 在新语义下的行为。
适用版本:Python 3.12+(实测 3.14.6;PEP 649/749 是 3.14 的新特性)

8.3 注解的求值时机(PEP 649/749)

上一节我们看到注解是运行时对象,get_type_hints 能把字符串注解解析成真实类型。但这里藏着一个更根本的问题:注解是在什么时候被求值的? 在 3.13 及以前,答案很简单——函数、类、模块定义的那一刻就求值了。这带来两个长期痛点:写一个引用尚未定义的类的注解,必须加引号;大量注解在程序启动时被无谓地求值,拖慢导入。Python 3.14 用 PEP 649 / PEP 749 改了这件事:注解被存进「专门的函数」,只在真正需要时才求值。

一、定义时不求值:一个 NameError 被推迟了

最直接的证明:写一个引用了不存在名字的注解,函数照样定义成功,直到你访问它的 __annotations__ 才报错:

import sys
print("python", sys.version.split()[0])

def f(x: Undefined_Name) -> Another_Missing:
    return x

print("定义成功,未抛错")
print("有 __annotate__ 吗:", hasattr(f, "__annotate__"))
try:
    f.__annotations__
except Exception as e:
    print("访问 __annotations__ 抛:", type(e).__name__, e)

输出(本机 3.14.6):

定义成功,未抛错
有 __annotate__ 吗: True
访问 __annotations__ 抛: NameError name 'Undefined_Name' is not defined

在 3.13 及以前,这个 def 会在定义时就抛 NameError。现在它被推迟到了访问注解的那一刻——这正是 PEP 649 的核心。

二、__annotate__:注解被编译成一个专门的函数

注解被求值的逻辑并没有消失,而是被编译器打包成了一个挂在函数上的 __annotate__:

import dis
def f(x: int, y: "int") -> bool: ...
print("签名:", __import__("inspect").signature(f.__annotate__))
dis.dis(f.__annotate__)

输出(本机 3.14.6,反汇编):

签名: (format, /)
  2           RESUME                   0
              LOAD_FAST_BORROW         0 (format)
              LOAD_SMALL_INT           2
              COMPARE_OP             132 (>)
              POP_JUMP_IF_FALSE        3 (to L1)
              NOT_TAKEN
              LOAD_COMMON_CONSTANT     1 (NotImplementedError)
              RAISE_VARARGS            1
      L1:     LOAD_CONST               1 ('x')
              LOAD_GLOBAL              0 (int)
              LOAD_CONST               2 ('y')
              LOAD_CONST               3 ('int')
              LOAD_CONST               4 ('return')
              LOAD_GLOBAL              2 (bool)
              BUILD_MAP                3
              RETURN_VALUE

反汇编里有两条关键信息:

  • 它接受一个位置参数 format,并且开头就检查 format > 2 就抛 NotImplementedError。也就是说,编译器生成的 __annotate__ 只原生支持 Format.VALUE(值 1)和内部的 VALUE_WITH_FAKE_GLOBALS(值 2);遇到 FORWARDREF(3)、STRING(4) 会抛 NotImplementedError,交给 annotationlib 兜底。
  • 它把注解表达式编译进函数体:LOAD_GLOBAL int 是真正求值,而字符串注解 "int" 被当成 LOAD_CONST 'int' 原样保留。

函数、类、模块都有 __annotate__:

class C:
    x: int
print(hasattr(C, "__annotate__"))          # True
import sys
print(hasattr(sys.modules[__name__], "__annotate__"))   # True

三、annotationlib 的三种 Format

标准库新增的 annotationlib 提供了在三种格式间取注解的能力:

import annotationlib
from annotationlib import Format

def f(x: Undefined_Name) -> Another_Missing: ...

for fmt in (Format.VALUE, Format.FORWARDREF, Format.STRING):
    try:
        print(fmt.name, "->", annotationlib.get_annotations(f, format=fmt))
    except Exception as e:
        print(fmt.name, "-> 抛", type(e).__name__)

输出:

VALUE -> 抛 NameError
FORWARDREF -> {'x': ForwardRef('Undefined_Name', owner=<function f ...>), 'return': ForwardRef('Another_Missing', owner=<function f ...>)}
STRING -> {'x': 'Undefined_Name', 'return': 'Another_Missing'}

三种格式的含义(Format 的枚举值是 VALUE=1, VALUE_WITH_FAKE_GLOBALS=2, FORWARDREF=3, STRING=4):

Format返回未定义名字用途
VALUE真实类型对象抛 NameError等价旧版 __annotations__
FORWARDREF真实值 + ForwardRef 标记包成 ForwardRef,不报错容忍未定义(dataclasses 已改用它)
STRING字符串原样字符串近似还原源码写法

实现细节值得记一笔:编译器生成的 __annotate__ 对这两种格式都抛 NotImplementedError,于是 annotationlib.call_annotate_function 会用同一个 code object 重新构造一个函数来跑——STRING 给它一个「每次名字查找都返回 _Stringifier 对象」的 globals,从而把表达式还原成字符串;FORWARDREF 则给它真实的 globals/builtins,再把查不到的名字包成 ForwardRef。这就是为什么 __annotate__(Format.FORWARDREF) 直接调会抛 NotImplementedError,而 annotationlib.get_annotations(..., format=Format.FORWARDREF) 却能正常工作。

四、求值只发生一次,结果被缓存

延迟求值不等于每次都算。注解第一次被访问时求值,之后直接命中缓存:

calls = []
def probe() -> int:
    calls.append(1)
    return int

def g(a: probe()) -> str:      # 注解是一个有副作用的调用表达式
    return "x"

print("定义后 calls =", calls)          # []
g.__annotations__
print("第一次访问后 calls =", calls)     # [1]
g.__annotations__; g.__annotations__
print("再访问两次后 calls =", calls)     # [1]
print("两次是同一对象:", g.__annotations__ is g.__annotations__)   # True

输出:

定义后 calls = []
第一次访问后 calls = [1]
再访问两次后 calls = [1]
两次是同一对象: True

缓存由 C 层负责——__annotations__ 现在是 function / type / module 上的一个 getset 描述符(<attribute '__annotations__' of 'function' objects>),而不是 __dict__ 里的普通键。这也意味着 3.14 起不能再通过 obj.__dict__["__annotations__"] 直接读到注解。

五、前向引用与循环引用不再需要引号

既然定义时不求值,写「引用尚未定义的类」的注解就不必加引号了。两个互相引用的类,谁先谁后都无所谓:

class A:
    def link(self, other: "B") -> None: ...
class B:
    def link(self, other: "A") -> None: ...

import typing
print(typing.get_type_hints(A.link))    # {'other': <class 'B'>, 'return': <class 'NoneType'>}
print(typing.get_type_hints(B.link))    # {'other': <class 'A'>, 'return': <class 'NoneType'>}

输出:

{'other': <class '__main__.B'>, 'return': <class 'NoneType'>}
{'other': <class '__main__.A'>, 'return': <class 'NoneType'>}

循环引用之所以能解,是因为求值被推迟到 get_type_hints 调用的那一刻——那时两个类都已经在命名空间里了。

六、from __future__ import annotations 的关系

那这个老朋友在 3.14 变成什么样了?实测结论是行为没变:它仍然把所有注解字符串化,__annotations__ 直接返回字符串,且完全不求值(所以未定义的名字也不会报错):

from __future__ import annotations
import annotationlib
from annotationlib import Format

class Node:
    def __init__(self, nxt: Node | None = None): ...

def h(a: Nope) -> None: ...

print(Node.__init__.__annotations__)   # {'nxt': 'Node | None'}  —— 字符串
print(h.__annotations__)               # {'a': 'Nope', 'return': 'None'}  —— 不求值也不报错
print(annotationlib.get_annotations(h, format=Format.STRING))

输出:

{'nxt': 'Node | None'}
{'a': 'Nope', 'return': 'None'}
{'a': 'Nope', 'return': 'None'}

Python 3.14 的 What’s New 明确写着:“In Python 3.14, the behavior of code using from __future__ import annotations is unchanged.” 同时它已被弃用,计划在未来版本移除(但不会早于 3.13 于 2029 年 EOL 之后)。所以新代码应直接依赖 PEP 649 的惰性求值,把 from __future__ import annotations 视为过渡工具。

七、typing.get_type_hints 在新语义下的行为

typing.get_type_hints 仍然是把注解解析成真实类型的首选入口,它内部用的就是上面这套机制:能解析就返回真实对象,遇到未定义名字仍抛 NameError:

import typing
def ok(x: int, y: "ok") -> bool: ...
print(typing.get_type_hints(ok))     # {'x': <class 'int'>, 'y': <function ok ...>, 'return': <class 'bool'>}

def bad(x: Nope) -> None: ...
try:
    typing.get_type_hints(bad)
except NameError as e:
    print("bad ->", type(e).__name__, e)   # bad -> NameError name 'Nope' is not defined

另外一条容易被忽略的收紧:3.14 起,实例不再能读到所属类的注解。这在以前是「未文档化的偶然行为」:

class C:
    x: int
print(C.__annotations__)               # {'x': <class 'int'>}
try:
    print(C().__annotations__)
except AttributeError as e:
    print("实例访问 ->", e)            # 实例访问 -> 'C' object has no attribute '__annotations__'

要跨版本安全地读注解,官方建议只使用 annotationlib 的公开 API(get_annotations、get_annotate_from_class_namespace),不要直接读类型对象的命名空间字典。typing_extensions 提供了 Format 与 get_annotations 的向后移植,可用来写兼容 3.14 前后两套语义的代码。想回顾注解本身的静态用法,见 Python 编程入门 · 类型注解与工具链 。

小结

  • 3.14 起(PEP 649/749)函数、类、模块的注解不在定义时求值,改为访问 __annotations__ 时才求值;引用未定义名字不再让定义失败。
  • 注解被编译器打包成 __annotate__ 函数,它只原生支持 Format.VALUE,其余格式由 annotationlib.call_annotate_function 通过重建函数兜底。
  • annotationlib 提供 VALUE / FORWARDREF / STRING 三种格式:分别对应「求值、容忍未定义、还原源码字符串」。
  • 求值结果会被缓存;__annotations__ 现在是函数/类/模块上的 getset 描述符,不再存于 __dict__。
  • 前向引用与循环引用天然可用,无需加引号;from __future__ import annotations 在 3.14 行为不变(仍字符串化),但已弃用。
  • typing.get_type_hints 仍可解析字符串注解,未定义名字仍抛 NameError;实例读类注解的旧行为已被移除。

类型系统讲到这里,边界已经清楚:注解只是运行时对象,静态检查器负责在编译前读懂它们。下一章 ctypes 与 cffi 调用 C 会离开纯 Python,进入扩展模块与跨语言调用。

阅读导航:上一节:typing 的静态与运行时边界 · 下一节:ctypes 与 cffi 调用 C 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章

  1. 《Python高级编程》目录
  2. 《Python高级编程》11.3 PEP 流程与版本迁移策略
  3. 《Python高级编程》11.2 嵌入式与自由线程运行时