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 3107 和 PEP 526):注解在源代码中遇到时会立即求值。
字符串化注解(在 Python 3.7 及更高版本中与
from __future__ import annotations
一起使用;参见 PEP 563):注解仅以字符串形式存储。延迟求值(Python 3.14 及更高版本的默认行为;参见 PEP 649 和 PEP 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
派生自的注解的对象,例如模块对象、类型对象或函数对象。globals、locals 和 type_params 参数提供了一种更精确的机制,用于影响求值
ForwardRef
时可用的名称。globals 和 locals 会传递给eval()
,表示求值名称时所在的全局和局部命名空间。type_params 参数与使用 泛型类 和 函数 的原生语法创建的对象相关。它是在求值正向引用时作用域内的 类型形参 元组。例如,如果要求值从泛型类C
的类命名空间中找到的注解中检索到的ForwardRef
,type_params 应设置为C.__type_params__
。由
get_annotations()
返回的ForwardRef
实例保留了对其来源作用域信息的引用,因此不带任何其他参数调用此方法可能足以求值此类对象。通过其他方式创建的ForwardRef
实例可能没有任何关于其作用域的信息,因此可能需要向此方法传递参数才能成功求值它们。如果没有提供 owner、globals、locals 或 type_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)¶
使用给定的 format(
Format
枚举的成员)调用注解函数 annotate,并返回该函数生成的注解字典。需要此辅助函数是因为编译器为函数、类和模块生成的注解函数在直接调用时仅支持
VALUE
格式。为了支持其他格式,此函数在一个特殊的环境中调用注解函数,使其能够以其他格式生成注解。在实现需要在类构建过程中部分求值注解的功能时,这是一个有用的构建块。owner 是拥有注解函数的对象,通常是函数、类或模块。如果提供,它将在
FORWARDREF
格式中用于生成携带更多信息的ForwardRef
对象。参见
PEP 649 中包含了对此函数所用实现技术的解释。
在 3.14 版本加入。
- annotationlib.call_evaluate_function(evaluate, format, *, owner=None)¶
使用给定的 format(
Format
枚举的成员)调用求值函数 evaluate,并返回该函数生成的值。这与call_annotate_function()
类似,但后者总是返回一个将字符串映射到注解的字典,而此函数返回单个值。这旨在与为类型别名和类型形参相关的惰性求值元素生成的求值函数一起使用
typing.TypeVar.evaluate_bound()
,类型变量的边界typing.TypeVar.evaluate_default()
,类型变量的默认值typing.ParamSpec.evaluate_default()
,参数规格的默认值typing.TypeVarTuple.evaluate_default()
,类型变量元组的默认值
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
的值保持不变。
globals 和 locals 会传递给
eval()
;有关更多信息,请参阅eval()
的文档。如果 globals 或 locals 为None
,此函数可能会根据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 版本加入。
范例¶
在元类中使用注解¶
元类 可能希望在类创建期间检查甚至修改类主体中的注解。这样做需要从类命名空间字典中检索注解。对于使用 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
格式通常可以恢复原始源代码;其他则不受支持,意味着它们可能导致不正确的输出或错误。
以下是受支持的(有时有注意事项)
-
ast.Invert
(~
),ast.UAdd
(+
), 和ast.USub
(-
) 是受支持的ast.Not
(not
) 不受支持
ast.Dict
(使用**
解包时除外)ast.Call
(使用**
解包时除外)ast.Constant
(虽然不是常量的确切表示;例如,字符串中的转义序列会丢失;十六进制数会转换为十进制)ast.Attribute
(假设值不是常量)ast.Subscript
(假设值不是常量)ast.Starred
(*
解包)
以下是不受支持的,但在字符串化器遇到时会抛出一个信息性错误
ast.FormattedValue
(f-字符串;如果使用!r
等转换说明符,则不会检测到错误)ast.JoinedStr
(f-字符串)
以下是不受支持的,并导致不正确的输出
以下在注解作用域中是不允许的,因此不相关
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
对象。