注释最佳实践 — Python 文档
注释最佳实践
- 作者
- 拉里·黑斯廷斯
抽象的
本文档旨在封装使用注释字典的最佳实践。 如果您编写 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'"}
。 这不应真正被视为“怪癖”; 在这里提到它只是因为它可能令人惊讶。