注释最佳实践¶
- 作者:
Larry Hastings
在 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.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__
成员,您应该确保它是一个字典,然后再尝试检查其内容。您应该避免修改
__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'"}
。这实际上不应该被认为是“怪癖”;这里提到它仅仅是因为它可能令人惊讶。