对象注解属性的最佳实践

在 Python 3.10 以上版本中访问对象的注解字典

Python 3.10 在标准库中加入了一个新函数:inspect.get_annotations()。在 Python 3.10 以上的版本中,调用该函数就是访问对象注解字典的最佳做法。该函数还可以“解析”字符串形式的注解。
有 时 会 因 为 某 些 原 因 看 不 到 inspect.get_annotations() , 也 可 以 直 接 访 问
annotations 数据成员。这方面的最佳实践在 Python 3.10 中也发生了变化:从 Python3.10 开始,Python 函数、类和模块的 o.annotations 保证可用。如果确定是要查看这三种对象,只要利用 o.annotations 读取对象的注释字典即可。
不过其他类型的可调用对象可能就没有定义 annotations 属性,比如由 functools.
partial() 创建的可调用对象。当访问某个未知对象的 “annotations“ 时,Python 3.10 以上版本的最佳做法是带三个参数去调用 getattr() ,比如 getattr(o, ‘annotations’,
None)。

在 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 中。由于类不一定会定义注解,最好的做法是在类的 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.dict 可视为globals。
    但并不是所有注解字符串都可以通过 eval() 成功地转化为 Python 值。理论上,注解字符串中可以包含任何合法字符串,确实有一些类型提示的场合,需要用到特殊的 无法被解析的字符串来作注解。比如:
  • PEP 604 union types using |, before support for this was added to Python 3.10.
  • 运行时用不到的定义,只在 typing.TYPE_CHECKING 为 True 时才会导入。
    如果 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’”}。这不应算是个“坑”;只是因为可能会让人吃惊,所以才提一下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

中亿丰数字科技集团有限公司

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值