annotationlib — 用于内省注解的功能

在 3.14 版本加入。

源代码: Lib/annotationlib.py


annotationlib 模块提供了用于内省模块、类和函数上的 注解 的工具。

注解是 惰性求值 的,并且通常包含对创建注解时尚未定义的对象的正向引用。该模块提供了一组底层工具,可用于以可靠的方式检索注解,即使存在正向引用和其他边缘情况。

此模块支持以三种主要格式检索注解(参见 Format),每种格式最适合不同的用例

  • VALUE 对注解求值并返回其值。这是最直接的使用方式,但可能会引发错误,例如当注解包含对未定义名称的引用时。

  • FORWARDREF 为无法解析的注解返回 ForwardRef 对象,允许您在不求值的情况下检查注解。当您需要处理可能包含未解析正向引用的注解时,这很有用。

  • STRING 以字符串形式返回注解,类似于它在源文件中出现的方式。这对于希望以可读方式显示注解的文档生成器很有用。

get_annotations() 函数是用于检索注解的主要入口点。给定一个函数、类或模块,它会返回所请求格式的注解字典。此模块还提供了直接使用用于求值注解的 注解函数 的功能,例如 get_annotate_from_class_namespace()call_annotate_function(),以及用于处理 求值函数call_evaluate_function() 函数。

注意

此模块中的大多数功能可以执行任意代码;有关更多信息,请参阅安全部分

参见

PEP 649 提出了当前 Python 中注解工作方式的模型。

PEP 749 扩展了 PEP 649 的各个方面,并引入了 annotationlib 模块。

注解最佳实践 提供了使用注解的最佳实践。

typing-extensions 提供了 get_annotations() 的向后移植版本,可在早期版本的 Python 上运行。

注解的语义

注解的求值方式在 Python 3 的历史中发生了变化,目前仍然依赖于 future 导入。注解曾有过以下执行模型:

  • 标准语义(Python 3.0 到 3.13 的默认行为;参见 PEP 3107PEP 526):注解在源代码中遇到时会立即求值。

  • 字符串化注解(在 Python 3.7 及更高版本中与 from __future__ import annotations 一起使用;参见 PEP 563):注解仅以字符串形式存储。

  • 延迟求值(Python 3.14 及更高版本的默认行为;参见 PEP 649PEP 749):注解是惰性求值的,仅在访问时才求值。

例如,考虑以下程序

def func(a: Cls) -> None:
    print(a)

class Cls: pass

print(func.__annotations__)

其行为如下

  • 在标准语义下(Python 3.13及更早版本),它会在定义 func 的那一行抛出 NameError,因为此时 Cls 是一个未定义的名称。

  • 在字符串化注解下(如果使用了 from __future__ import annotations),它会打印 {'a': 'Cls', 'return': 'None'}

  • 在延迟求值下(Python 3.14及更高版本),它会打印 {'a': <class 'Cls'>, 'return': None}

当函数注解在 Python 3.0 中首次引入时(由 PEP 3107),使用了标准语义,因为这是实现注解最简单、最直接的方式。当变量注解在 Python 3.6 中引入时(由 PEP 526),也使用了相同的执行模型。然而,当使用注解作为类型提示时,标准语义会引起问题,例如需要引用在注解遇到时尚未定义的名称。此外,在模块导入时执行注解存在性能问题。因此,在 Python 3.7 中,PEP 563 引入了使用 from __future__ import annotations 语法将注解存储为字符串的功能。当时的计划是最终将此行为设为默认,但出现了一个问题:对于在运行时内省注解的人来说,字符串化注解更难处理。一个替代提案,PEP 649,引入了第三种执行模型,即延迟求值,并在 Python 3.14 中实现。如果存在 from __future__ import annotations,则仍使用字符串化注解,但此行为最终将被移除。

class annotationlib.Format

一个 IntEnum,描述了可以返回注解的格式。该枚举的成员或其等效的整数值可以传递给 get_annotations() 和此模块中的其他函数,以及 __annotate__ 函数。

VALUE = 1

值是求值注解表达式的结果。

VALUE_WITH_FAKE_GLOBALS = 2

用于表示注解函数正在具有虚假全局变量的特殊环境中求值的特殊值。当传递此值时,注解函数应返回与 Format.VALUE 格式相同的值,或引发 NotImplementedError 以表示它们不支持在此环境中执行。此格式仅在内部使用,不应传递给此模块中的函数。

FORWARDREF = 3

对于已定义的值,值为真实的注解值(根据 Format.VALUE 格式);对于未定义的值,值为 ForwardRef 代理。真实对象可能包含对 ForwardRef 代理对象的引用。

STRING = 4

值是注解在源代码中出现的文本字符串,可能会有包括但不限于空白规范化和常量值优化在内的修改。

这些字符串的确切值在 Python 的未来版本中可能会改变。

在 3.14 版本加入。

class annotationlib.ForwardRef

一个用于注解中正向引用的代理对象。

当使用 FORWARDREF 格式且注解包含无法解析的名称时,将返回此类的实例。当在注解中使用正向引用时,例如在定义类之前引用该类,就可能发生这种情况。

__forward_arg__

一个包含被求值以产生 ForwardRef 的代码的字符串。该字符串可能与原始源代码不完全等价。

evaluate(*, owner=None, globals=None, locals=None, type_params=None, format=Format.VALUE)

求值正向引用,返回其值。

如果 format 参数是 VALUE(默认值),此方法可能会抛出异常,例如 NameError,如果正向引用指向一个无法解析的名称。此方法的参数可用于为本来未定义的名称提供绑定。如果 format 参数是 FORWARDREF,该方法将永远不会抛出异常,但可能返回一个 ForwardRef 实例。例如,如果正向引用对象包含代码 list[undefined],其中 undefined 是一个未定义的名称,使用 FORWARDREF 格式求值它将返回 list[ForwardRef('undefined')]。如果 format 参数是 STRING,该方法将返回 __forward_arg__

owner 参数为此方法传递作用域信息提供了首选机制。ForwardRef 的所有者是包含该 ForwardRef 派生自的注解的对象,例如模块对象、类型对象或函数对象。

globalslocalstype_params 参数提供了一种更精确的机制,用于影响求值 ForwardRef 时可用的名称。globalslocals 会传递给 eval(),表示求值名称时所在的全局和局部命名空间。type_params 参数与使用 泛型类函数 的原生语法创建的对象相关。它是在求值正向引用时作用域内的 类型形参 元组。例如,如果要求值从泛型类 C 的类命名空间中找到的注解中检索到的 ForwardReftype_params 应设置为 C.__type_params__

get_annotations() 返回的 ForwardRef 实例保留了对其来源作用域信息的引用,因此不带任何其他参数调用此方法可能足以求值此类对象。通过其他方式创建的 ForwardRef 实例可能没有任何关于其作用域的信息,因此可能需要向此方法传递参数才能成功求值它们。

如果没有提供 ownerglobalslocalstype_params,并且 ForwardRef 不包含关于其来源的信息,则使用空的全局和局部字典。

在 3.14 版本加入。

函数

annotationlib.annotations_to_string(annotations)

将包含运行时值的注解字典转换为仅包含字符串的字典。如果值不是字符串,则使用 type_repr() 进行转换。这旨在作为用户提供的注解函数的辅助工具,这些函数支持 STRING 格式,但无法访问创建注解的代码。

例如,这用于为通过函数式语法创建的 typing.TypedDict 类实现 STRING

>>> from typing import TypedDict
>>> Movie = TypedDict("movie", {"name": str, "year": int})
>>> get_annotations(Movie, format=Format.STRING)
{'name': 'str', 'year': 'int'}

在 3.14 版本加入。

annotationlib.call_annotate_function(annotate, format, *, owner=None)

使用给定的 formatFormat 枚举的成员)调用注解函数 annotate,并返回该函数生成的注解字典。

需要此辅助函数是因为编译器为函数、类和模块生成的注解函数在直接调用时仅支持 VALUE 格式。为了支持其他格式,此函数在一个特殊的环境中调用注解函数,使其能够以其他格式生成注解。在实现需要在类构建过程中部分求值注解的功能时,这是一个有用的构建块。

owner 是拥有注解函数的对象,通常是函数、类或模块。如果提供,它将在 FORWARDREF 格式中用于生成携带更多信息的 ForwardRef 对象。

参见

PEP 649 中包含了对此函数所用实现技术的解释。

在 3.14 版本加入。

annotationlib.call_evaluate_function(evaluate, format, *, owner=None)

使用给定的 formatFormat 枚举的成员)调用求值函数 evaluate,并返回该函数生成的值。这与 call_annotate_function() 类似,但后者总是返回一个将字符串映射到注解的字典,而此函数返回单个值。

这旨在与为类型别名和类型形参相关的惰性求值元素生成的求值函数一起使用

owner 是拥有求值函数的对象,例如类型别名或类型变量对象。

format可用于控制返回值的格式

>>> type Alias = undefined
>>> call_evaluate_function(Alias.evaluate_value, Format.VALUE)
Traceback (most recent call last):
...
NameError: name 'undefined' is not defined
>>> call_evaluate_function(Alias.evaluate_value, Format.FORWARDREF)
ForwardRef('undefined')
>>> call_evaluate_function(Alias.evaluate_value, Format.STRING)
'undefined'

在 3.14 版本加入。

annotationlib.get_annotate_from_class_namespace(namespace)

从类命名空间字典 namespace 中检索注解函数。如果命名空间不包含注解函数,则返回 None。这主要在类完全创建之前(例如,在元类中)有用;类存在后,可以使用 cls.__annotate__ 检索注解函数。有关在元类中使用此函数的示例,请参见下文

在 3.14 版本加入。

annotationlib.get_annotations(obj, *, globals=None, locals=None, eval_str=False, format=Format.VALUE)

计算一个对象的注解字典。

obj 可以是可调用对象、类、模块或其他具有 __annotate____annotations__ 属性的对象。传递任何其他对象会引发 TypeError

format 参数控制返回注解的格式,并且必须是 Format 枚举的成员或其等效整数。不同格式的工作方式如下

  • VALUE: 首先尝试 object.__annotations__;如果不存在,则在存在的情况下调用 object.__annotate__ 函数。

  • FORWARDREF: 如果 object.__annotations__ 存在且可以成功求值,则使用它;否则,调用 object.__annotate__ 函数。如果它也不存在,则再次尝试 object.__annotations__ 并重新引发访问它时产生的任何错误。

  • STRING: 如果 object.__annotate__ 存在,则首先调用它;否则,使用 object.__annotations__ 并使用 annotations_to_string() 将其字符串化。

返回一个字典。每次调用 get_annotations() 都会返回一个新字典;在同一个对象上调用它两次将返回两个不同但等价的字典。

此函数为您处理了几个细节

  • 如果 eval_str 为 true,则类型为 str 的值将使用 eval() 进行反字符串化。这旨在与字符串化注解(from __future__ import annotations)一起使用。将 eval_str 设置为 true 并使用除 Format.VALUE 之外的格式是错误的。

  • 如果 obj 没有注解字典,则返回一个空字典。(函数和方法总是有一个注解字典;类、模块和其他类型的可调用对象可能没有。)

  • 忽略类上继承的注解,以及元类上的注解。如果一个类没有自己的注解字典,则返回一个空字典。

  • 为了安全起见,所有对对象成员和字典值的访问都是使用 getattr()dict.get() 完成的。

eval_str 控制是否将类型为 str 的值替换为对这些值调用 eval() 的结果

  • 如果 eval_str 为 true,则对类型为 str 的值调用 eval()。(注意 get_annotations() 不会捕获异常;如果 eval() 引发异常,它将展开堆栈,越过 get_annotations() 调用。)

  • 如果 eval_str 为 false(默认值),则类型为 str 的值保持不变。

globalslocals 会传递给 eval();有关更多信息,请参阅 eval() 的文档。如果 globalslocalsNone,此函数可能会根据 type(obj) 将该值替换为特定于上下文的默认值

  • 如果 obj 是一个模块,globals 默认为 obj.__dict__

  • 如果 obj 是一个类,globals 默认为 sys.modules[obj.__module__].__dict__locals 默认为 obj 类的命名空间。

  • 如果 obj 是一个可调用对象,globals 默认为 obj.__globals__,但如果 obj 是一个包装函数(使用 functools.update_wrapper())或一个 functools.partial 对象,它会被解包直到找到一个未包装的函数。

调用 get_annotations() 是访问任何对象注解字典的最佳实践。有关注解最佳实践的更多信息,请参阅注解最佳实践

>>> def f(a: int, b: str) -> float:
...     pass
>>> get_annotations(f)
{'a': <class 'int'>, 'b': <class 'str'>, 'return': <class 'float'>}

在 3.14 版本加入。

annotationlib.type_repr(value)

将任意 Python 值转换为适合 STRING 格式使用的格式。此函数对大多数对象调用 repr(),但对某些对象(如类型对象)有特殊处理。

这旨在作为用户提供的注解函数的辅助工具,这些函数支持 STRING 格式,但无法访问创建注解的代码。它也可用于为包含注解中常见值的其他对象提供用户友好的字符串表示。

在 3.14 版本加入。

范例

在元类中使用注解

元类 可能希望在类创建期间检查甚至修改类主体中的注解。这样做需要从类命名空间字典中检索注解。对于使用 from __future__ import annotations 创建的类,注解将位于字典的 __annotations__ 键中。对于其他带注解的类,可以使用 get_annotate_from_class_namespace() 获取注解函数,并使用 call_annotate_function() 调用它并检索注解。使用 FORWARDREF 格式通常是最好的,因为这允许注解引用在创建类时尚无法解析的名称。

要修改注解,最好创建一个包装器注解函数,该函数调用原始注解函数,进行任何必要的调整,然后返回结果。

下面是一个元类的示例,它从类中过滤掉所有 typing.ClassVar 注解,并将它们放在一个单独的属性中

import annotationlib
import typing

class ClassVarSeparator(type):
   def __new__(mcls, name, bases, ns):
      if "__annotations__" in ns:  # from __future__ import annotations
         annotations = ns["__annotations__"]
         classvar_keys = {
            key for key, value in annotations.items()
            # Use string comparison for simplicity; a more robust solution
            # could use annotationlib.ForwardRef.evaluate
            if value.startswith("ClassVar")
         }
         classvars = {key: annotations[key] for key in classvar_keys}
         ns["__annotations__"] = {
            key: value for key, value in annotations.items()
            if key not in classvar_keys
         }
         wrapped_annotate = None
      elif annotate := annotationlib.get_annotate_from_class_namespace(ns):
         annotations = annotationlib.call_annotate_function(
            annotate, format=annotationlib.Format.FORWARDREF
         )
         classvar_keys = {
            key for key, value in annotations.items()
            if typing.get_origin(value) is typing.ClassVar
         }
         classvars = {key: annotations[key] for key in classvar_keys}

         def wrapped_annotate(format):
            annos = annotationlib.call_annotate_function(annotate, format, owner=typ)
            return {key: value for key, value in annos.items() if key not in classvar_keys}

      else:  # no annotations
         classvars = {}
         wrapped_annotate = None
      typ = super().__new__(mcls, name, bases, ns)

      if wrapped_annotate is not None:
         # Wrap the original __annotate__ with a wrapper that removes ClassVars
         typ.__annotate__ = wrapped_annotate
      typ.classvars = classvars  # Store the ClassVars in a separate attribute
      return typ

STRING 格式的局限性

STRING 格式旨在近似注解的源代码,但所使用的实现策略意味着并不总是可能恢复确切的源代码。

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

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

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

备注

这个特定示例在撰写本文时有效,但它依赖于实现细节,不保证将来仍然有效。

在 Python 中存在的不同种类的表达式中,如 ast 模块所表示的,一些表达式是受支持的,这意味着 STRING 格式通常可以恢复原始源代码;其他则不受支持,意味着它们可能导致不正确的输出或错误。

以下是受支持的(有时有注意事项)

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

以下是不受支持的,并导致不正确的输出

以下在注解作用域中是不允许的,因此不相关

FORWARDREF 格式的局限性

FORWARDREF 格式旨在尽可能地生成真实值,任何无法解析的内容都用 ForwardRef 对象替换。它受到与 STRING 格式大致相同的限制:对字面量执行操作或使用不受支持的表达式类型的注解,在使用 FORWARDREF 格式求值时可能会引发异常。

以下是一些关于不受支持表达式行为的例子

>>> from annotationlib import get_annotations, Format
>>> def zerodiv(x: 1 / 0): ...
>>> get_annotations(zerodiv, format=Format.STRING)
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero
>>> get_annotations(zerodiv, format=Format.FORWARDREF)
Traceback (most recent call last):
  ...
ZeroDivisionError: division by zero
>>> def ifexp(x: 1 if y else 0): ...
>>> get_annotations(ifexp, format=Format.STRING)
{'x': '1'}

内省注解的安全影响

此模块中的许多功能都涉及执行与注解相关的代码,这些代码可以执行任意操作。例如,get_annotations() 可能会调用任意的 注解函数,而 ForwardRef.evaluate() 可能会对任意字符串调用 eval()。注解中包含的代码可能会进行任意的系统调用、进入无限循环或执行任何其他操作。这也适用于任何对 __annotations__ 属性的访问,以及 typing 模块中处理注解的各种函数,例如 typing.get_type_hints()

由此产生的任何安全问题也立即适用于导入可能包含不受信任注解的代码之后:导入代码总是可能导致执行任意操作。然而,从不受信任的来源接受字符串或其他输入,并将其传递给任何用于内省注解的 API 是不安全的,例如通过编辑 __annotations__ 字典或直接创建 ForwardRef 对象。