Python - PEP 749 – PEP 649 的实现补充
摘要
本 PEP 补充了 PEP 649 的规范,具体包括:
from __future__ import annotations(PEP 563)将至少保留到 Python 3.13 生命周期结束,之后会弃用并最终移除。- 新增标准库模块
annotationlib,为注解提供工具,包括get_annotations()、注解格式枚举、ForwardRef类及辅助的__annotate__调用函数。 - REPL 环境下的注解与模块级注解一样延迟求值。
- 明确诸如
classmethod()和functools.wraps()等包装器提供注解时的行为。 - 不再用代码标志位区分“可在伪 globals 环境执行的 annotate”,而是新增
VALUE_WITH_FAKE_GLOBALS格式供第三方实现表明支持的格式。 - 直接删除
__annotations__属性时也会清除__annotate__。 - 支持 PEP 695、PEP 696 增加的类型别名值、类型参数约束/默认值等用类似 PEP 649 的延迟求值方式。
SOURCE格式更名为STRING,提升清晰度。- 条件定义的类和模块注解能正确处理。
- 若在部分执行的模块访问注解,返回已执行部分的注解,但不缓存。
动机
PEP 649 为 Python 注解提供了更优雅的延迟求值机制,极大提升了类型提示和运行时类型系统的可用性、优雅性和能力。PEP 649 最初计划在 3.10 实现,2023 年被正式接受,但实现延期至 3.14。
在实现过程中发现 PEP 649 部分细节未明确定义,个别角落行为也有值得改进之处。本 PEP 不替代而是补充 PEP 649,对部分细节加以完善,提升整体用户体验。
from __future__ import annotations 的未来
PEP 563 引入 future import annotations,让所有注解都变为字符串。PEP 649 提出无需 future import 的新方案,并计划弃用 563,但未具体说明弃用步骤。
规范
建议的弃用计划如下:
- Python 3.14 及以前,
from __future__ import annotations行为不变,注解全为字符串。- 若激活该 future import,带注解对象的
__annotate__返回字符串形式(VALUE 格式),与__annotations__保持一致。
- 若激活该 future import,带注解对象的
- 在不支持 PEP 649 的最后一个 Python 版本(预计为 3.13)停止维护后,开始弃用该 future import。使用时编译器发出
DeprecationWarning。弃用最早从 3.13 EOL 后第一个大版本开始,也可延后。 - 至少两个大版本后,彻底移除该 future import,注解始终按 PEP 649 延迟求值。继续使用则抛出
SyntaxError。
被拒绝的方案
- 立即让 future import 失效:会破坏那些依赖 forwardref 且 eager 求值的代码,影响面广,不可行。
- 立即弃用 future import:许多库依赖该特性实现前向引用,若立即弃用无法兼容所有 Python 版本且避免警告,不现实。
- 永久保留 future import:会导致 Python 语义永远分裂为两种模式,不利于语言一致性。
- 未来让 future import 变为无操作:不如明确要求用户移除 future import,避免歧义。
新增 annotationlib 模块
PEP 649 建议把注解相关工具加入 inspect,但该模块过大且依赖众多,性能不佳。我们决定新建 annotationlib,专门提供注解工具,也便于后续扩展。
原理
PEP 649 建议用 typing.ForwardRef 实现 FORWARDREF 格式,但现有 typing.ForwardRef 与 typing 模块耦合,且对外文档不足但被三方库大量依赖。我们将其替换为 annotationlib.ForwardRef,并兼容原有用法。保留 _evaluate 方法但标记为弃用,同时在 typing 新增 evaluate_forward_ref,专门用于类型提示下的前向引用求值。
新增 annotationlib.call_annotate_function 辅助调用 __annotate__,便于如 typing.NamedTuple 之类在类构造过程中部分求值注解。
规范
标准库新增 annotationlib:
get_annotations():获取函数、模块、类注解,将取代inspect.get_annotations()(后者将委托前者,暂不弃用)。get_annotate_from_class_namespace(namespace):从类命名空间获取__annotate__。Format:注解格式枚举,含 VALUE、VALUE_WITH_FAKE_GLOBALS、FORWARDREF、STRING(原 PEP 649 的 VALUE、FORWARDREF、SOURCE)。ForwardRef:前向引用类,含__forward_arg__、evaluate()等。call_annotate_function(func, format):辅助调用__annotate__,若不支持给定格式则自动用 fake globals 环境。call_evaluate_function(func, format):类似上,但用于 PEP 695/696 的延迟属性求值。annotations_to_string(annotations):将注解 dict 全部转为字符串形式。type_repr(value):单个对象转字符串,类型用完全限定名,其他用 repr()。
typing 模块新增 evaluate_forward_ref,递归求值前向引用,特化于类型提示。
PEP 649 的 VALUE、FORWARDREF、SOURCE 等常量不会作为 inspect 全局成员,只能通过 annotationlib.Format.VALUE 等访问。
被拒绝的方案
- 其他名称(如 annotation、annotools、annotationslib、annotationlib 等),最终选择了
annotationlib。 - 加入 inspect/typing/types 模块,理由同上,不便于隔离注解与类型提示、类型本身。
- 作为三方包开发,标准库自身已需用这些功能,直接内置更好。
- 先做成私有模块,后公开。实际标准库与三方很快都会用,直接设计正式 API 更合适。
REPL 行为
PEP 649 原计划 REPL 下注解立即求值,与模块行为不同。但这样实现更复杂且会让用户疑惑。我们建议 REPL 下也延迟求值,与模块一致。
每一行含注解的全局代码会生成新的 __annotate__,前一行的 __annotate__ 会被覆盖。REPL 没有全局命名空间的 __annotations__ 键。
类和函数定义同样延迟注解求值,annotationlib 可用于 introspect。
包装器对象的注解
标准库包装器(如 functools.wraps、classmethod、staticmethod)应:
- 尽可能延迟求值,保持一致性和兼容性。
functools.update_wrapper/wraps只复制__annotate__,__annotations__用复制的__annotate__。classmethod/staticmethod构造器提供可写的__annotate__和__annotations__,读取时从被包裹对象取并缓存,写入时只改自身,不影响被包裹对象。
元类与注解
发现 metaclass 与 class 注解交互存在老 bug,比如 metaclass 的注解会意外影响子类。PEP 649 延迟求值后,__annotate__、__annotations__ 只在特定条件下设置,依赖 type 描述符,若 metaclass 字典有同名键会导致描述符失效,出现异常。
最终方案:在类字典存储 __annotate__/__annotations__ 时用内部专用名,避免与 type 描述符冲突。用户不应直接访问类字典获取注解,应用 annotationlib.get_annotate_from_class_namespace。
被拒绝的方案
- 总是/从不在类字典插入注解相关键,或用特殊描述符,都有实现/兼容性/复杂性权衡。最终选择内部隐藏存储。
新增 VALUE_WITH_FAKE_GLOBALS 格式
PEP 649 原建议用 code flag 指示 __annotate__ 是否能在 fake globals 环境运行。我们改为新增枚举值 VALUE_WITH_FAKE_GLOBALS,编译器生成的 annotate 函数支持该格式,第三方手写的不支持时应抛 NotImplementedError。
规范
annotationlib.Format 增加 VALUE_WITH_FAKE_GLOBALS,值为 2。其余格式依次递增。函数如不支持该格式应抛 NotImplementedError。
删除 annotations 的效果
PEP 649 规定设置 __annotations__ 会同步置 __annotate__ 为 None,但未说明删除时如何处理。本 PEP 补充:删除 __annotations__ 时也应将 __annotate__ 设为 None。
PEP 695/696 对象的延迟求值
PEP 695/696 新增的类型别名值、类型参数约束/默认值等已用延迟求值,但未暴露底层函数。现新增:
typing.TypeAliasType.evaluate_valuetyping.TypeVar.evaluate_bound、evaluate_constraints、evaluate_defaulttyping.ParamSpec.evaluate_defaulttyping.TypeVarTuple.evaluate_default
属性为只读,未设置时为 None。可配合 annotationlib.call_evaluate_function 求值。
dataclass 字段类型行为
延迟求值后,dataclass 字段类型可用前向引用,但 field.type 属性保留 ForwardRef,不自动求值。文档应指导用户用 ForwardRef.evaluate 显式求值。未来可视需求增加自动求值方法。
SOURCE 更名为 STRING
原 SOURCE 格式旨在还原源码,但实际上只能生成字符串,且不总能还原原始源代码,故更名为 STRING。现在支持四种格式:
- VALUE
- VALUE_WITH_FAKE_GLOBALS
- FORWARDREF
- STRING
条件定义的注解
PEP 649 原不支持类/模块体内条件定义注解,但实际用得较多(如 SQLAlchemy)。本 PEP 采用唯一标识记录已执行注解,__annotate__ 只返回执行过的注解。
部分执行模块的注解缓存
PEP 649 原实现部分执行模块时返回空注解且缓存,现规定:此时返回已执行的注解但不缓存,后续访问会重新调用 __annotate__,包含新增注解。
实现细节
ForwardRef只实现少量方法,FORWARDREF 格式下实际用内部 stringizer,最终转换为 ForwardRef 对象。__annotate__函数的参数名format为仅限位置参数,避免注解中符号冲突。
向后兼容性
PEP 649 已详述所有代码的兼容性影响。本 PEP 主要补充新代码与旧 introspection 工具的兼容问题,如装饰器等需升级支持 FORWARDREF 格式。标准库相关工具已更新以兼容本 PEP 规范。
安全影响
PEP 649 使得访问注解可能执行任意代码(包括 STRING 格式),但历史上访问注解后续操作(如 isinstance、typing 工具、repr)本就可能执行任意代码,且只有已导入代码才会触发。
教学建议
PEP 649 及本 PEP 的延迟注解语义对普通用户直观友好,无需手动加引号。需要 introspect 的高级用户可参考 annotationlib 文档。
参考实现
本 PEP 变更已在 CPython 主分支实现。
致谢
感谢 Larry Hastings 撰写 PEP 649,Carl Meyer、Alex Waygood、Alyssa Coghlan、David Ellis、Nikita Sobolev 等在本 PEP 制定和标准库实现中的贡献。
附录:哪些表达式能被 stringizer 还原
PEP 649 承认 stringizer 不能还原所有表达式。可还原的 AST 节点包括:
- BinOp
- UnaryOp(
~、+、-支持,not不支持) - Dict(除
**展开) - Set
- Compare(
==、!=、<、<=、>、>=支持,但部分顺序可能变化,is、in等不支持) - Call(除
**展开) - Constant(但转义等信息丢失)
- Attribute(前提不是常量)
- Subscript(前提不是常量)
- Starred
- Name
- List
- Tuple
- Slice
不支持或报错:
- FormattedValue、JoinedStr(f-string)
- BoolOp(and、or)、IfExp、Lambda、推导式、生成器表达式(结果错误)
注解作用域禁止 NamedExpr、Await、Yield 等。
版权
本文档可置于公有领域或 CC0-1.0-Universal 许可下,以更宽松者为准。
原文地址:https://peps.python.org/pep-0749/
1059

被折叠的 条评论
为什么被折叠?



