注解最佳实践

作者:

Larry Hastings

在 Python 3.10 及更高版本中访问对象的注解字典

Python 3.10 在标准库中添加了一个新函数:inspect.get_annotations()。在 Python 3.10 到 3.13 版本中,调用此函数是访问任何支持注解的对象的注解字典的最佳实践。此函数还可以为您“反字符串化”字符串化的注解。

在 Python 3.14 中,有一个新的 annotationlib 模块,其中包含用于处理注解的功能。这包括一个 annotationlib.get_annotations() 函数,它取代了 inspect.get_annotations()

如果由于某种原因 inspect.get_annotations() 不适用于您的用例,您可以手动访问 __annotations__ 数据成员。从 Python 3.10 开始,最佳实践也发生了变化:o.__annotations__ 保证 始终 适用于 Python 函数、类和模块。如果您确定您正在检查的对象是这三个 特定 对象之一,您可以直接使用 o.__annotations__ 来获取对象的注解字典。

然而,其他类型的可调用对象——例如,由 functools.partial() 创建的可调用对象——可能没有定义 __annotations__ 属性。在访问可能未知对象的 __annotations__ 时,Python 3.10 及更高版本中的最佳实践是调用带有三个参数的 getattr(),例如 getattr(o, '__annotations__', None)

在 Python 3.10 之前,访问未定义注解但其父类有注解的类的 __annotations__ 将返回父类的 __annotations__。在 Python 3.10 及更高版本中,子类的注解将是一个空字典。

在 Python 3.9 及更早版本中访问对象的注解字典

在 Python 3.9 及更早版本中,访问对象的注解字典比在新版本中复杂得多。问题在于这些旧版本的 Python 中的一个设计缺陷,特别是与类注解有关的缺陷。

访问其他对象(函数、其他可调用对象和模块)的注解字典的最佳实践与 3.10 版本的最佳实践相同,前提是您没有调用 inspect.get_annotations():您应该使用带有三个参数的 getattr() 来访问对象的 __annotations__ 属性。

不幸的是,这对于类来说不是最佳实践。问题在于,由于 __annotations__ 在类上是可选的,并且由于类可以从其基类继承属性,访问类的 __annotations__ 属性可能会无意中返回 基类 的注解字典。例如

class Base:
    a: int = 3
    b: str = 'abc'

class Derived(Base):
    pass

print(Derived.__annotations__)

这将打印 Base 的注解字典,而不是 Derived 的。

如果您正在检查的对象是一个类 (isinstance(o, type)),您的代码将不得不有一个单独的代码路径。在这种情况下,最佳实践依赖于 Python 3.9 及以前版本的实现细节:如果一个类定义了注解,它们将存储在类的 __dict__ 字典中。由于类可能定义或未定义注解,最佳实践是调用类字典上的 get() 方法。

总而言之,以下是一些示例代码,用于在 Python 3.9 及以前版本中安全地访问任意对象的 __annotations__ 属性

if isinstance(o, type):
    ann = o.__dict__.get('__annotations__', None)
else:
    ann = getattr(o, '__annotations__', None)

运行此代码后,ann 应该是一个字典或 None。建议您在进一步检查之前使用 isinstance() 再次检查 ann 的类型。

请注意,某些奇异或格式错误的类型对象可能没有 __dict__ 属性,因此为了额外安全,您可能还需要使用 getattr() 来访问 __dict__

手动反字符串化字符串化注解

在某些注解可能被“字符串化”的情况下,并且您希望评估这些字符串以生成它们所代表的 Python 值,最好还是调用 inspect.get_annotations() 来为您完成这项工作。

如果您正在使用 Python 3.9 或更早版本,或者由于某种原因您无法使用 inspect.get_annotations(),您将需要复制其逻辑。建议您检查当前 Python 版本中 inspect.get_annotations() 的实现并遵循类似的方法。

简而言之,如果您希望评估任意对象 o 上的字符串化注解

  • 如果 o 是一个模块,在调用 eval() 时使用 o.__dict__ 作为 globals

  • 如果 o 是一个类,在调用 eval() 时使用 sys.modules[o.__module__].__dict__ 作为 globals,并使用 dict(vars(o)) 作为 locals

  • 如果 o 是一个使用 functools.update_wrapper()functools.wraps()functools.partial() 包装的可调用对象,通过访问 o.__wrapped__o.func 迭代地解包它,直到找到根未包装函数。

  • 如果 o 是一个可调用对象(但不是类),在调用 eval() 时使用 o.__globals__ 作为全局变量。

然而,并非所有用作注解的字符串值都可以通过 eval() 成功转换为 Python 值。理论上,字符串值可以包含任何有效字符串,实际上,有些类型提示的有效用例需要使用 无法 评估的字符串值进行注解。例如

  • PEP 604 联合类型使用 |,在 Python 3.10 中添加支持之前。

  • 在运行时不需要的定义,仅当 typing.TYPE_CHECKING 为真时才导入。

如果 eval() 尝试评估这些值,它将失败并引发异常。因此,在设计处理注解的库 API 时,建议仅在调用者明确请求时才尝试评估字符串值。

适用于任何 Python 版本的 __annotations__ 最佳实践

  • 您应该避免直接为对象的 __annotations__ 成员赋值。让 Python 管理 __annotations__ 的设置。

  • 如果您确实直接为对象的 __annotations__ 成员赋值,您应该始终将其设置为 dict 对象。

  • 您应该避免直接访问任何对象上的 __annotations__。相反,请使用 annotationlib.get_annotations() (Python 3.14+) 或 inspect.get_annotations() (Python 3.10+)。

  • 如果您确实直接访问对象的 __annotations__ 成员,您应该在尝试检查其内容之前确保它是一个字典。

  • 您应该避免修改 __annotations__ 字典。

  • 您应该避免删除对象的 __annotations__ 属性。

__annotations__ 怪癖

在所有 Python 3 版本中,如果对象上没有定义注解,函数对象会惰性创建注解字典。您可以使用 del fn.__annotations__ 删除 __annotations__ 属性,但如果您随后访问 fn.__annotations__,对象将创建一个新的空字典,并将其存储并作为其注解返回。在函数惰性创建其注解字典之前删除函数上的注解将引发 AttributeError;连续两次使用 del fn.__annotations__ 保证始终引发 AttributeError

上一段中的所有内容也适用于 Python 3.10 及更高版本中的类和模块对象。

在所有 Python 3 版本中,您可以将函数对象上的 __annotations__ 设置为 None。然而,随后使用 fn.__annotations__ 访问该对象上的注解将根据本节第一段的规定惰性创建空字典。这对于任何 Python 版本中的模块和类都 适用;这些对象允许将 __annotations__ 设置为任何 Python 值,并将保留设置的任何值。

如果 Python 为您字符串化注解(使用 from __future__ import annotations),并且您将字符串指定为注解,则该字符串本身将被引用。实际上,注解被引用了 两次。例如

from __future__ import annotations
def foo(a: "str"): pass

print(foo.__annotations__)

这将打印 {'a': "'str'"}。这不应该真正被视为“怪癖”;这里提到它只是因为它可能令人惊讶。

如果您使用具有自定义元类的类并访问类上的 __annotations__,您可能会观察到意外行为;有关一些示例,请参见 749。您可以通过在 Python 3.14+ 上使用 annotationlib.get_annotations() 或在 Python 3.10+ 上使用 inspect.get_annotations() 来避免这些怪癖。在早期版本的 Python 中,您可以通过从类的 __dict__ 访问注解来避免这些错误(例如,cls.__dict__.get('__annotations__', None))。

在某些 Python 版本中,类的实例可能具有 __annotations__ 属性。然而,这不是支持的功能。如果您需要实例的注解,您可以使用 type() 访问其类(例如,在 Python 3.14+ 上使用 annotationlib.get_annotations(type(myinstance)))。