Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

PEP 749 – 实现 PEP 649

作者:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
讨论列表:
Discourse 帖子
状态:
草稿
类型:
标准跟踪
主题:
类型提示
依赖:
649
创建:
2024年5月28日
Python 版本:
3.14
历史记录:
2024年6月4日

目录

摘要

本 PEP 通过对 PEP 649 的规范进行各种调整和补充来补充它。

  • from __future__ import annotations (PEP 563) 将至少在 Python 3.13 达到生命周期结束之前继续保持其当前行为。随后,它将被弃用并最终移除。
  • 添加了一个新的标准库模块 annotationlib,用于提供注解的工具。它将包含 get_annotations() 函数、注解格式的枚举、ForwardRef 类以及用于调用 __annotate__ 函数的辅助函数。
  • REPL 中的注解是延迟求值的,就像其他模块级注解一样。
  • 我们指定了提供注解的包装器对象的行为,例如 classmethod() 和使用 functools.wraps() 的代码。
  • 将不会有代码标志用于标记可以在“伪全局变量”环境中运行的 __annotate__ 函数。
  • 直接设置 __annotations__ 属性不会影响 __annotate__ 属性。
  • 我们添加了功能,允许使用类似 PEP 649 的语义来评估类型别名值以及类型参数边界和默认值(由 PEP 695PEP 696 添加)。

动机

PEP 649 为创建 Python 中更好的注解语义提供了极好的框架。它解决了注解用户(包括使用静态类型提示的用户以及使用运行时类型的用户)的常见痛点,并且使语言更优雅、更强大。该 PEP 最初于 2021 年针对 Python 3.10 提出,并于 2023 年被接受。然而,实现比预期的花费了更长的时间,现在预计该 PEP 将在 Python 3.14 中实现。

我已开始在 CPython 中着手实现该 PEP。我发现该 PEP 在某些方面存在未充分说明的地方,并且它在某些极端情况下的决定值得商榷。此新的 PEP 提出了对规范的若干更改和补充,以解决这些问题。

本 PEP 补充而非取代 PEP 649。此处提出的更改应该可以改善整体用户体验,但它们不会改变早期 PEP 的总体框架。

from __future__ import annotations 的未来

PEP 563 之前引入了 future import from __future__ import annotations,它将所有注解更改为字符串。 PEP 649 提出了一种不需要此 future import 的替代方法,并声明

如果接受本 PEP,PEP 563 将被弃用并最终移除。

但是,该 PEP 没有提供此弃用的详细计划。

之前有一些关于此主题的讨论 在 Discourse 上(请注意,在链接的帖子中,我提出了与此处提出的内容不同的内容)。

规范

我们建议以下弃用计划

  • 在 Python 3.14 中,from __future__ import annotations 将继续像以前一样工作,将注解转换为字符串。
  • 在最后一个不支持 PEP 649 语义(预计为 3.13)的版本达到生命周期结束之后,from __future__ import annotations 将被弃用。编译任何使用 future import 的代码都会发出 DeprecationWarning。这将在 Python 3.13 达到生命周期结束后的第一个版本中发生,但社区可能会决定等待更长时间。
  • 至少经过两个版本后,future import 将被移除,并且注解将始终根据 PEP 649 进行求值。继续使用 future import 的代码将引发 SyntaxError,类似于任何其他未定义的 future import。

被拒绝的替代方案

立即使 future import 成为无操作:我们考虑在 Python 3.14 中将 PEP 649 语义应用于所有代码,使 future import 成为无操作。但是,这将破坏在以下条件集下在 3.13 中工作的代码

  • __future__ import annotations 处于活动状态
  • 存在依赖于前向引用的注解
  • 注解在导入时被急切地求值,例如由元类或类或函数装饰器求值。例如,这目前适用于已发布版本的 typing_extensions.TypedDict

预计这将是一种常见的模式,因此我们无法承受在从 3.13 升级到 3.14 的过程中破坏此类代码。

此类代码在 future import 最终被移除时仍然会中断。但是,这将在未来很多年后发生,这使得受影响的装饰器有足够的时间来更新它们的代码。

立即弃用 future import:我们可以立即开始在使用 future import 时发出警告,而不是等到 Python 3.13 达到生命周期结束。但是,许多库已经使用 from __future__ import annotations 作为一种优雅的方式来在其注解中启用不受限制的前向引用。如果我们立即弃用 future import,那么这些库将无法在所有受支持的 Python 版本上使用不受限制的前向引用,同时避免弃用警告:与从标准库中弃用的其他功能不同,__future__ import 必须是给定模块中的第一条语句,这意味着不可能只在 Python 3.13 及更低版本上条件导入 __future__.annotations。(必要的 sys.version_info 检查将计为在 __future__ import 之前的一条语句。)

永远保留 future import:我们还可以决定无限期地保留 future import。但是,这将永久地使 Python 语言的行为出现分歧。这是不可取的;语言应该只有一组语义,而不是两种永久不同的模式。

将来使 future import 成为无操作:我们可以决定在 Python 3.13 达到生命周期结束后的某个时间点,使 from __future__ import annotations 变成什么也不做,而不是最终将其变成 SyntaxError。这仍然存在上面关于现在将其变为无操作的一些相同问题,尽管生态系统将有更长的时间来适应。最好让用户将来在确认他们不依赖于字符串化注解后,显式地从他们的代码中删除 future import。

新的 annotationlib 模块

PEP 649 提出将与注解相关的工具添加到 inspect 模块中。但是,该模块相当庞大,直接或间接依赖于至少 35 个其他标准库模块,并且导入速度非常慢,以至于其他标准库模块通常不鼓励导入它。此外,我们预计除了 inspect.get_annotations() 函数和 VALUEFORWARDREFSOURCE 格式之外,还会添加更多工具。

一个新的标准库模块为该功能提供了一个逻辑位置,并且还使我们能够添加更多对注解使用者有用的工具。

基本原理

PEP 649 指出,应该使用 typing.ForwardRef 来实现 inspect.get_annotations() 中的 FORWARDREF 格式。但是,typing.ForwardRef 的现有实现与 typing 模块的其余部分交织在一起,因此将 typing 特定的行为添加到通用的 get_annotations() 函数中没有意义。此外,typing.ForwardRef 是一个有问题的类:它是公开且有文档记录的,但文档中未列出其任何属性或方法。尽管如此,第三方库还是使用了它的一些未记录的属性。例如,PydanticTypeguard 使用了 _evaluate 方法;beartypepyanalyze 使用了 __forward_arg__ 属性。

我们将现有的但定义不明确的 typing.ForwardRef 替换为一个新类 annotationlib.ForwardRef。它旨在与 typing.ForwardRef 类的现有用法基本兼容,但不会包含 typing 模块特有的行为。为了与现有用户兼容,我们保留了私有 _evaluate 方法,但将其标记为已弃用。它委托给 typing 模块中的一个新的公共函数 typing.evaluate_forward_ref,该函数旨在以特定于类型提示的方式评估前向引用。

我们添加了一个函数 annotationlib.call_annotate_function 作为调用 __annotate__ 函数的辅助函数。当实现需要在类构造过程中部分评估注释的功能时,这是一个有用的构建块。例如,typing.NamedTuple 的实现需要在命名元组类本身构造之前从类命名空间字典中检索注释,因为注释决定了命名元组上存在哪些字段。

规范

一个新的模块 annotationlib 被添加到标准库中。其目的是提供用于内省和包装注释的工具。

模块的确切内容尚未确定。我们将向使用注释的标准库功能(例如 dataclassestyping.TypedDict)添加对 PEP 649 语义的支持,并利用经验来指导新模块的设计。

该模块将包含以下功能

  • get_annotations():一个返回函数、模块或类的注释的函数。这将替换 inspect.get_annotations()。后者将委托给新函数。它最终可能会被弃用,但为了最大程度地减少中断,我们不建议立即弃用。
  • get_annotate_function():一个返回对象 __annotate__ 函数的函数(如果存在),或者返回 None(如果不存在)。这通常等同于访问 .__annotate__ 属性,除了元类存在的情况下(参见下面)。
  • Format:一个枚举,包含注释的可能格式。这将替换 PEP 649 中的 VALUEFORWARDREFSOURCE 格式。PEP 649 建议将这些值设为 inspect 模块的全局成员;我们更倾向于将它们放在枚举中。
  • ForwardRef:表示前向引用的类;当格式为 FORWARDREF 时,它可能会被 get_annotations() 返回。现有的类 typing.ForwardRef 将成为此类的别名。其成员包括
    • __forward_arg__:前向引用的字符串参数
    • evaluate(globals=None, locals=None, type_params=None, owner=None):尝试评估前向引用的方法。ForwardRef 对象可能会保存对其来源对象的全局变量和其他命名空间的引用。如果是这样,这些命名空间可用于评估前向引用。owner 参数可能是保存原始注释的对象,例如类或模块对象;如果未提供这些命名空间,则它用于提取全局变量和局部变量命名空间。
    • _evaluate(),与现有的 ForwardRef._evaluate 方法具有相同的接口。它将不会有文档记录,并且会立即弃用。它是为了与 typing.ForwardRef 的现有用户兼容而提供的。
  • call_annotate_function(func: Callable, format: Format):用于使用给定格式调用 __annotate__ 函数的辅助函数。如果函数不支持此格式,则 call_annotate_function() 将设置一个“伪全局变量”环境(如 PEP 649 中所述),并使用该环境返回所需的注释格式。
  • call_evaluate_function(func: Callable | None, format: Format):类似于 call_annotate_function,但不依赖于函数返回注释字典。这旨在用于评估由 PEP 695PEP 696 引入的延迟属性;有关详细信息,请参见下文。func 可以为 None 以方便起见;如果传递了 None,则该函数也会返回 None

一个新的函数也添加到 typing 模块中,即 typing.evaluate_forward_ref。此函数是 ForwardRef.evaluate 方法的包装器,但它执行了特定于类型提示的其他工作。例如,它递归进入复杂类型并在这些类型中评估其他前向引用。

PEP 649 不同,注释格式(VALUEFORWARDREFSOURCE)不会作为 inspect 模块的全局成员添加。引用这些常量的唯一推荐方法是 annotationlib.Format.VALUE

未解决的问题

这个模块应该叫什么?一些想法

  • annotations:最明显的名称,但它可能会与现有的 from __future__ import annotations 造成混淆,因为用户可能在同一个模块中同时使用 import annotationsfrom __future__ import annotations。使用常用词作为名称将使模块更难搜索。有一个 PyPI 包 annotations,但它在 2015 年只发布了一个版本,看起来已被放弃。
  • annotools:类似于 itertoolsfunctools,但“anno” 比“iter”或“func”不那么明显的缩写。截至撰写本文时,没有名为此名称的 PyPI 包。
  • annotationtools:一个更明确的版本。有一个 PyPI 包 annotationtools,并在 2023 年发布过。
  • annotation_tools:上面版本的变体,但避免了 PyPI 冲突。然而,其他标准库模块的名称中都没有下划线。
  • annotationslib:类似于 tomllibpathlibimportlib。PyPI 上没有同名的包。
  • annotationlib:与上面类似,但短了一个字符,并且主观上读起来更好。在 PyPI 上也没有被占用。

被拒绝的替代方案

将功能添加到 inspect 模块:如上所述,inspect 模块已经非常庞大,并且在某些用例中,其导入时间令人望而却步。

将功能添加到 typing 模块:虽然注释主要用于类型提示,但它们也可以用于其他目的。我们更倾向于在用于内省注释的功能和专门用于类型提示的功能之间保持清晰的分隔。

将功能添加到 types 模块types 模块旨在提供与类型相关的功能,而注释可以存在于函数和模块上,而不仅仅是类型上。

在一个第三方包中开发此功能:此新模块中的功能将是纯 Python 代码,并且可以通过直接与解释器生成的 __annotate__ 函数交互来实现提供相同功能的第三方包。但是,所提议的新模块的功能肯定会在标准库本身中用到(例如,用于实现 dataclassestyping.NamedTuple),因此将其包含在标准库中是有意义的。

将此功能添加到一个私有模块:可以先在一个私有的标准库模块(例如,_annotations)中开发该模块,并在我们积累了更多 API 使用经验后才将其公开。但是,我们已经知道标准库本身将需要该模块的部分功能(例如,用于实现 dataclassestyping.NamedTuple)。即使我们将其设为私有,该模块也必然会被第三方用户使用。最好从一开始就提供一个清晰的、有文档的 API,以便第三方用户能够像标准库一样彻底地支持 PEP 649 语义。该模块将立即在标准库的其他部分使用,确保它涵盖了一组合理的用例。

REPL 的行为

PEP 649 指定了交互式 REPL 的以下行为

为了简单起见,在这种情况下,我们放弃了延迟求值。REPL shell 中的模块级注释将继续像“普通语义”那样工作,立即求值并将结果直接设置到 __annotations__ 字典中。

这种提议的行为存在一些问题。它使 REPL 成为唯一一个注释仍然立即求值的上下文,这会让用户感到困惑,并使语言复杂化。

它还会使 REPL 的实现更加复杂,因为它需要确保所有语句都以“交互式”模式编译,即使它们的输出不需要显示。(如果一行中有多个语句由 REPL 求值,则这一点很重要。)

最重要的是,这破坏了一些新手用户可能会遇到的合理用例。用户可能会在文件中编写以下内容

a: X | None = None
class X: ...

PEP 649 中,这将正常工作:当 X 用于 a 的注释时,它尚未定义,但注释是延迟求值的。但是,如果用户将相同的代码粘贴到 REPL 中并逐行执行,它将抛出 NameError,因为名称 X 尚未定义。

此主题之前已在 Discourse 上讨论过。

规范

我们建议将交互式控制台视为任何其他模块级代码,并使注释延迟求值。这使语言更加一致,并避免了模块和 REPL 之间细微的行为变化。

由于 REPL 是逐行求值的,因此我们将为全局作用域中包含注释的每个求值语句生成一个新的 __annotate__ 函数。每当求值一行包含注释的语句时,先前的 __annotate__ 函数就会丢失

>>> x: int
>>> __annotate__(1)
{'x': <class 'int'>}
>>> y: str
>>> __annotate__(1)
{'y': <class 'str'>}
>>> z: doesntexist
>>> __annotate__(1)
Traceback (most recent call last):
File "<python-input-5>", line 1, in <module>
    __annotate__(1)
    ~~~~~~~~~~~~^^^
File "<python-input-4>", line 1, in __annotate__
    z: doesntexist
       ^^^^^^^^^^^
NameError: name 'doesntexist' is not defined

REPL 的全局命名空间中将没有 __annotations__ 键。在模块命名空间中,当访问模块对象的 __annotations__ 描述符时,此键会被延迟创建,但在 REPL 中没有这样的模块对象。

在 REPL 中定义的类和函数也将像任何其他类一样工作,因此它们的注释求值将被延迟。可以访问 __annotations____annotate__ 属性或使用 annotationlib 模块来内省注释。

提供 __annotations__ 的包装器

标准库和其他地方的几个对象为其包装的对象提供注释。PEP 649 没有指定此类包装器应该如何行为。

规范

提供注释的包装器应牢记以下目标

  • 应尽可能延迟 __annotations__ 的求值,这与内置函数、类和模块的行为一致。
  • 应保留与实现 PEP 649 之前行为的向后兼容性。
  • 应同时为 __annotate____annotations__ 属性提供与其包装对象一致的语义。

更具体地说

  • functools.update_wrapper()(以及因此的 functools.wraps())将仅将 __annotate__ 属性从被包装对象复制到包装器。包装器函数上的 __annotations__ 描述符将使用复制的 __annotate__
  • classmethod()staticmethod() 的构造函数当前将 __annotations__ 属性从被包装对象复制到包装器。相反,它们将具有 __annotate____annotations__ 的可写属性。读取这些属性将从底层可调用对象检索相应的属性并将其缓存在包装器的 __dict__ 中。写入这些属性将直接更新 __dict__,而不会影响被包装的可调用对象。

注解和元类

对本 PEP 初始实现的测试揭示了元类和类注释之间交互的严重问题。

已存在的问题

在调查本 PEP 中要指定的行为时,我们发现 __annotations__ 在类上的现有行为中存在一些错误。在 Python 3.13 及更早版本上修复这些错误超出了本 PEP 的范围,但这里记录了它们以解释需要处理的极端情况。

作为背景,在 Python 3.10 到 3.13 上,如果类有任何注释,则 __annotations__ 字典将被放置在类命名空间中。如果没有,在创建类时,__annotations__ 类字典键不存在,但访问 cls.__annotations__ 会调用在 type 上定义的描述符,该描述符返回一个空字典并将其存储在类字典中。静态类型 是一个例外:它们永远不会有注释,访问 .__annotations__ 会引发 AttributeError。在 Python 3.9 及更早版本上,行为有所不同;请参阅 gh-88067

以下代码在 Python 3.10 到 3.13 上的行为完全相同

class Meta(type): pass

class X(metaclass=Meta):
    a: str

class Y(X): pass

Meta.__annotations__  # important
assert Y.__annotations__ == {}, Y.__annotations__  # fails: {'a': <class 'str'>}

如果在访问 Y 上的注释之前访问了元类 Meta 上的注释,则基类 X 的注释会被泄漏到 Y。但是,如果没有访问元类的注释(即,删除上面的 Meta.__annotations__ 行),则 Y 的注释将正确地为空。

类似地,来自带注释元类的注释会泄漏到未带注释且是该元类实例的类中

class Meta(type):
    a: str

class X(metaclass=Meta):
    pass

assert X.__annotations__ == {}, X.__annotations__  # fails: {'a': <class 'str'>}

这些行为的原因是,如果元类在其类字典中包含一个__annotations__条目,这将阻止元类的实例在基类type上使用__annotations__数据描述符。在第一种情况下,访问Meta.__annotations__会产生Meta.__dict__["__annotations__"] = {}的副作用。然后,在Y上查找__annotations__属性时,首先会看到元类属性,但会跳过它,因为它是一个数据描述符。接下来,它会在其方法解析顺序 (MRO) 中类的类字典中查找,找到X.__annotations__并返回它。在第二个示例中,MRO 中任何地方都没有注释,因此type.__getattribute__回退到返回元类属性。

PEP 649 中的元类行为

使用PEP 649,在涉及元类时访问类上的.__annotations__属性的行为变得更加不稳定,因为现在即使对于带有注释的类,__annotations__也只会被延迟添加到类字典中。新的__annotate__属性也会在没有注释的类上被延迟创建,这会导致在涉及元类时出现更多错误行为。

这些问题的原因是我们只在某些情况下设置__annotate____annotations__类字典条目,并依赖于type上定义的描述符来填充它们(如果它们未设置)。当使用正常的属性查找时,这种方法在存在元类的情况下会失效,因为元类自身类字典中的条目会使描述符不可见。

虽然我们考虑了几种允许cls.__annotations__cls.__annotate__cls是具有自定义元类的类型时可靠工作的方法,但任何此类方法都会向高级用户暴露很大的复杂性。相反,我们推荐一个更简单的方法,将复杂性限制在annotationlib模块中:在annotationlib.get_annotations中,我们通过直接使用type.__annotations__描述符来绕过正常的属性查找。

规范

用户应该始终使用annotationlib.get_annotations来访问类对象的注释,并使用annotationlib.get_annotate_function来访问__annotate__函数。即使涉及元类,这些函数也只会返回类自身的注释。

访问具有除builtins.type之外的元类的类的__annotations____annotate__属性的行为未定义。文档应警告不要直接使用这些属性,并建议改用annotationlib模块。

类似地,类字典中存在__annotations____annotate__键是实现细节,不应依赖于它。

被拒绝的替代方案

我们考虑了两种处理类中__annotations____annotate__条目的行为的广泛方法

  • 确保该条目始终存在于类字典中,即使它为空或尚未被评估。这意味着我们不必依赖于type上定义的描述符来填充该字段,因此元类的属性不会干扰。(在gh-120719中进行原型设计。)
  • 确保该条目永远不存在于类字典中,或者至少永远不会由语言核心中的逻辑添加。这意味着type上的描述符将始终被使用,而不会受到元类的干扰。(在gh-120816中进行原型设计。)

Alex Waygood 建议使用第一种方法进行实现。当创建堆类型(例如通过class语句创建的类)时,cls.__dict__["__annotations__"]被设置为一个特殊的描述符。在__get__上,描述符通过调用__annotate__并返回结果来评估注释。注释字典缓存在描述符实例中。描述符的行为也像一个映射,因此使用cls.__dict__["__annotations__"]的代码通常仍然可以工作:将对象视为映射将评估注释并表现得好像描述符本身就是注释字典。(但是,假设cls.__dict__["__annotations__"]dict的特定实例的代码可能会失效。)

对于__annotate__,这种方法也易于实现:此属性对于带有注释的类始终被设置,并且我们可以为没有注释的类将其显式设置为None

虽然这种方法可以修复与元类相关的已知边缘情况,但它会为所有类引入显著的复杂性,包括一个具有异常行为的新内置类型(用于注释描述符)。

另一种方法是永远不设置__dict__["__annotations__"],并使用其他存储来存储缓存的注释。这种行为更改甚至必须应用于在from __future__ import annotations下定义的类,因为否则,如果一个类是在没有from __future__ import annotations的情况下定义的,但其元类启用了 future,则可能会出现错误的行为。如PEP 649先前所述,从类字典中删除__annotations__也会带来向后兼容性影响:cls.__dict__.get("__annotations__")是检索注释的常用习惯用法。

这种方法还意味着在带注释类的实例上访问.__annotations__不再起作用。虽然此行为未在文档中说明,但它是 Python 的一项长期存在的特性,并且一些用户依赖于它。

删除用于标记 __annotate__ 函数的代码标志

PEP 649 指定

此 PEP 假设第三方库可能会实现自己的__annotate__方法,并且这些函数在在此“伪全局”环境中运行时几乎肯定无法正常工作。因此,此 PEP 在代码对象上分配了一个标志,即co_flags中未使用的位之一,表示“此代码对象可以在‘伪全局’环境中运行”。这使得“伪全局”环境严格地选择加入,并且预计只有由 Python 编译器生成的__annotate__方法会设置它。

在我们为标准库添加PEP 649支持的工作中,我们没有发现对这种机制的需求。虽然自定义__annotate__函数可能无法很好地与“伪全局”环境配合使用,但这项技术仅在__annotate__函数引发NotImplementedError以表示它不支持请求的格式时使用。但是,手动实现的__annotate__函数可能会支持所有三种注释格式;通常,它们将包含对annotationlib.call_annotate_function的调用以及对结果的一些转换。

此外,提议的机制将实现与代码对象的低级细节耦合。代码对象标志是 CPython 特定的,并且文档明确警告不要依赖于这些值。

规范

标准库将在任何__annotate__函数上使用“伪全局”技术,该函数在不支持请求的格式时引发NotImplementedError

实现__annotate__函数的第三方代码应支持所有三种注释格式,或准备处理“伪全局”环境。这应该在__annotate__的数据模型文档中提及。

设置 __annotations__ 的影响

PEP 649 指定

o.__annotations__设置为合法值会自动将o.__annotate__设置为None

我们希望在写入__annotations__时保持__annotate__不变。从概念上讲,__annotate__提供了基本事实,而__annotations__仅仅是一个缓存,如果缓存被修改,我们不应该丢弃基本事实。

PEP 649 行为背后的动机是保持这两个属性的同步。但是,这在一般情况下是不可能的;如果 __annotations__ 字典被就地修改,这不会反映在 __annotate__ 属性中。如果设置 __annotations____annotate__ 没有影响,则该区域的整体思维模型将更简单。

规范

当设置 __annotations__ 时,__annotate__ 的值不会改变。

PEP 695 和 696 对象的延迟求值

自从 PEP 649 编写以来,Python 3.12 和 3.13 开始支持几个新的特性,这些特性也使用延迟求值,类似于本 PEP 对注解提出的行为。

目前,这些对象使用延迟求值,但无法直接访问用于延迟求值的函数对象。为了启用现在对注解可用的相同类型的内省,我们建议公开内部函数对象,允许用户使用 FORWARDREF 和 SOURCE 格式对其进行求值。

规范

我们将添加以下新的属性

除了 evaluate_value 之外,如果对象没有绑定、约束或默认值,则这些属性可能为 None。否则,该属性是一个可调用对象,类似于 __annotate__ 函数,它接受一个整数参数并返回求值结果。与 __annotate__ 函数不同,这些可调用对象返回单个值,而不是注解字典。这些属性是只读的。

通常,用户会将这些属性与 annotationlib.call_evaluate_function 结合使用。例如,要以 SOURCE 格式获取 TypeVar 的绑定,可以编写 annotationlib.call_evaluate_function(T.evaluate_bound, annotationlib.Format.SOURCE)

其他实现细节

PEP 649 对实现的某些方面进行了详细的讨论。为了避免混淆,我们描述了当前实现与 PEP 中描述的一些方面的差异。但是,这些细节不能保证在将来保持不变,并且它们可能会在将来未经通知而更改,除非它们在语言参考中进行了记录。

ForwardRef 对象支持的操作

“SOURCE” 格式由“字符串化器”技术实现,其中函数的全局字典被增强,以便每次查找都导致一个特殊的对象,该对象可用于重建对该对象执行的操作。

PEP 649 指定

在实践中,“字符串化器”功能将在当前在 typing 模块中定义的 ForwardRef 对象中实现。 ForwardRef 将扩展为实现所有字符串化器功能;它还将扩展为支持评估它包含的字符串,以生成真实值(假设所有引用的符号都已定义)。

但是,这可能会在实践中导致混淆。实现字符串化器功能的对象必须实现几乎所有特殊方法,包括 __getattr____eq__,以返回一个新的字符串化器。这样的对象使用起来令人困惑:所有操作都成功,但它们可能返回与用户期望不同的对象。

当前实现仅在 ForwardRef 类上实现了几个有用的方法。在注解求值期间,使用私有字符串化器类的实例而不是 ForwardRef。求值完成后,FORWARDREF 格式的实现将这些内部对象转换为 ForwardRef 对象。

__annotate__ 函数的签名

PEP 649 将 __annotate__ 函数的签名指定为

__annotate__(format: int) -> dict

但是,如果注解使用名为 format 的类,则使用 format 作为参数名称可能会导致冲突。为了避免此问题,参数应为仅限位置的,并且应具有不能作为合法标识符的名称。

当前实现使用以点开头的名称 .format,但确切的名称应被视为实现细节,不能依赖于它。

为了简单起见,文档可能仍然使用名称 format

向后兼容性

PEP 649 对使用标准或 PEP 563 语义的现有代码的后向兼容性影响进行了彻底的讨论。

但是,还有一组兼容性问题:假设 PEP 649 语义编写的新的代码,但使用了急切地求值注解的现有工具。例如,考虑一个类似于 dataclass 的类装饰器 @annotator,它通过直接访问 __annotations__ 或调用 inspect.get_annotations() 来检索其装饰的类中的带注解的字段。

一旦 PEP 649 实现,这样的代码将正常工作

class X:
    y: Y

class Y: pass

但这不会,除非 @annotator 被更改为使用新的 FORWARDREF 格式。

@annotator
class X:
    y: Y

class Y: pass

这不是严格的后向兼容性问题,因为之前工作的代码不会中断;在 PEP 649 之前,此代码将在运行时引发 NameError。从某种意义上说,它与任何其他需要第三方库支持的新 Python 特性没有什么不同。然而,对于执行内省的库来说,这是一个严重的问题,并且重要的是我们使库能够以简单、用户友好的方式支持新语义变得尽可能容易。

我们将更新标准库中受此问题影响的部分,并且我们建议将常用的功能添加到新的 annotationlib 模块中,以便第三方工具可以使用相同的工具集。

安全影响

无。

如何教授

PEP 649 的语义(由本 PEP 修改)对于向其代码添加注解的用户来说应该很大程度上是直观的。我们消除了手动在需要前向引用的注解周围添加引号的需要,这是用户混淆的主要来源。

对于需要内省注解的高级用户,情况变得更加复杂。新 annotationlib 模块的文档将作为需要以编程方式与注解交互的用户参考。

参考实现

正在进行的 PR #119891 实现了很多 PEP 的内容。

未解决的问题

当我们在实现 PEP 的过程中取得进展时,我们可能会发现 PEP 649 需要澄清或修改的其他领域。鼓励读者关注跟踪 PEP 实现的 CPython 问题 并试用草稿实现。任何反馈都可能纳入此 PEP 的未来版本。

dataclass 字段类型是否应该使用延迟求值?

当前草案实现已支持数据类中的延迟求值,因此这可以工作。

>>> from dataclasses import dataclass
>>> @dataclass
... class D:
...     x: undefined
...

但是,FORWARDREF 格式泄漏到数据类的字段类型中。

>>> fields(D)[0].type
ForwardRef('undefined')

我们可以改为为字段类型添加延迟求值,类似于上面为类型别名值概述的方法。

访问 .type 可能会抛出错误。

>>> @dataclass
... class D:
...     x: undefined
...
>>> field = fields(D)[0]
>>> field.type
Traceback (most recent call last):
  File "<python-input-4>", line 1, in <module>
    field.type
  File ".../dataclasses.py", line 308, in type
    annos = self._annotate(annotationlib.Format.VALUE)
  File "<python-input-2>", line 3, in __annotate__
    x: undefined
      ^^^^^^^^^
NameError: name 'undefined' is not defined

但用户可以使用 annotationlib.call_evaluate_function 以其他格式获取类型。

>>> annotationlib.call_evaluate_function(field.evaluate_type, annotationlib.Format.SOURCE)
'undefined'
>>> annotationlib.call_evaluate_function(field.evaluate_type, annotationlib.Format.FORWARDREF)
ForwardRef('undefined')

其他变体也是可能的。例如,我们可以保持 type 属性不变,只添加 evaluate_type 方法。这避免了访问 .type 可能抛出异常时令人不愉快的意外情况。

致谢

首先,我要感谢 Larry Hastings 撰写了 PEP 649。此 PEP 修改了他的一些初始决策,但总体设计仍然是他提出的。

我感谢 Carl Meyer 和 Alex Waygood 对此 PEP 的早期草案提供的反馈。Alex Waygood、Alyssa Coghlan 和 David Ellis 对元类和 __annotations__ 之间的交互提供了有见地的反馈和建议。

附录

哪些表达式可以被转换为字符串?

PEP 649 承认字符串化器无法处理所有表达式。现在我们有了草案实现,我们可以更精确地说明可以和不能处理的表达式。下面列出了字符串化器可以和不能恢复的所有 Python AST 表达式。完整的列表可能不应该添加到文档中,但创建它是一个有用的练习。

首先,字符串化器当然无法恢复编译代码中不存在的任何信息,包括注释、空格、括号和 AST 优化器简化的操作。

其次,字符串化器可以拦截几乎所有涉及在某个作用域中查找名称的操作,但它无法拦截完全对常量进行操作的操作。作为推论,这也意味着在不受信任的代码上请求 SOURCE 格式是不安全的:Python 足够强大,即使没有访问任何全局变量或内置函数,也可能实现任意代码执行。例如

>>> def f(x: (1).__class__.__base__.__subclasses__()[-1].__init__.__builtins__["print"]("Hello world")): pass
...
>>> annotationlib.get_annotations(f, format=annotationlib.Format.SOURCE)
Hello world
{'x': 'None'}

(这个特定的例子在我的 PEP 草案的当前实现中对我有用;确切的代码可能在将来无法继续工作。)

以下内容受支持(有时存在注意事项)

  • BinOp
  • UnaryOp
    • Invert (~)、UAdd (+) 和 USub (-) 受支持
    • Not (not) 不受支持
  • Dict(除非使用 ** 解包)
  • Set
  • Compare
    • EqNotEq 受支持
    • LtLtEGtGtE 受支持,但操作数可能会被翻转
    • IsIsNotInNotIn 不受支持
  • Call(除非使用 ** 解包)
  • Constant(尽管不是常量的精确表示;例如,字符串中的转义序列会丢失;十六进制数字会转换为十进制)
  • Attribute(假设该值不是常量)
  • Subscript(假设该值不是常量)
  • Starred (* 解包)
  • Name
  • List
  • Tuple
  • Slice

以下内容不受支持,但在字符串化器遇到时会抛出信息性错误

  • FormattedValue(f-字符串;如果使用 !r 等转换说明符,则不会检测到错误)
  • JoinedStr(f-字符串)

以下内容不受支持,会导致输出不正确

  • BoolOp (andor)
  • IfExp
  • Lambda
  • ListComp
  • SetComp
  • DictComp
  • GeneratorExp

以下内容在注释作用域中不允许使用,因此与之无关

  • NamedExpr (:=)
  • Await
  • Yield
  • YieldFrom

来源:https://github.com/python/peps/blob/main/peps/pep-0749.rst

上次修改时间:2024-07-23 20:49:18 GMT