注释最佳实践 — Python 文档

来自菜鸟教程
Python/docs/3.10/howto/annotations
跳转至:导航、​搜索

注释最佳实践

作者
拉里·黑斯廷斯

抽象的

本文档旨在封装使用注释字典的最佳实践。 如果您编写 Python 代码来检查 Python 对象上的 __annotations__,我们鼓励您遵循下面描述的准则。

该文档分为四个部分:在 Python 3.10 及更高版本中访问对象注释的最佳实践,在 Python 3.9 及更早版本中访问对象注释的最佳实践,__annotations__ 的其他最佳实践适用于任何 Python 版本,以及 __annotations__ 的怪癖。

请注意,本文档专门针对使用 __annotations__,而不是将 用于 注释。 如果您正在寻找有关如何在代码中使用“类型提示”的信息,请参阅 typing 模块。


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

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

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

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


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

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

访问其他对象(函数、其他可调用对象和模块)的注释字典的最佳实践与 3.10 的最佳实践相同,假设您没有调用 inspect.get_annotations():您应该使用三个-argument 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( )
  • 如果 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 时,建议仅在调用者明确请求时才尝试评估字符串值。


__annotations__ 在任何 Python 版本中的最佳实践

  • 您应该避免直接分配给对象的 __annotations__ 成员。 让 Python 管理设置 __annotations__
  • 如果您确实直接分配给对象的 __annotations__ 成员,则应始终将其设置为 dict 对象。
  • 如果直接访问对象的 __annotations__ 成员,则应在尝试检查其内容之前确保它是字典。
  • 您应该避免修改 __annotations__ dicts。
  • 您应该避免删除对象的 __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'"}。 这不应真正被视为“怪癖”; 在这里提到它只是因为它可能令人惊讶。