Python 教程

Python 教程 Python 简介 Python 历史 Python 下载安装 Python 入门 Python 语法 Python 注释 Python 变量 Python 数据类型 Python 数值类型 Python 类型转换 Python 字符串 Python 布尔值 Python 运算符 Python 列表 Python 元组 Python 集合 Python 字典 Python If...Else Python While 循环 Python For 循环 Python 函数 Python Lambda Python 数组 Python 类和对象 Python 继承 Python 迭代 Python 作用域 Python 模块 Python 日期时间 Python 数学运算 Python JSON Python 正则表达式 Python PIP Python Try...Except Python 用户输入 Python 字符串格式化

Python 文件处理

Python 文件处理 Python 打开文件 Python 创建/写入文件 Python 删除文件

Python NumPy

NumPy 简介 NumPy 入门 NumPy 创建数组 NumPy 数组索引 NumPy 数组裁切 NumPy 数据类型 NumPy 副本 vs 视图 NumPy 数组形状 NumPy 数组重塑 NumPy 数组迭代 NumPy 数组连接 NumPy 数组拆分 NumPy 数组搜索 NumPy 数组排序 NumPy 数组过滤 NumPy 随机数 NumPy ufunc 通用函数

Python SciPy

SciPy 简介 SciPy 入门 SciPy 常量 SciPy 优化器 SciPy 稀疏数据 SciPy 图表 SciPy 空间数据 SciPy Matlab 数组 SciPy 插值 SciPy 统计显着性检验

Python 机器学习

Machine 机器学习入门 Machine 平均中位数模式 Machine 标准差 Machine 百分位数 Machine 数据分布 Machine 正态数据分布 Machine 散点图 Machine 线性回归 Machine 多项式回归 Machine 多元回归 Machine 缩放 Machine 训练/测试 Machine 决策树

Python MySQL

MySQL 入门 MySQL Create Database MySQL Create Table MySQL Insert MySQL Select MySQL Where MySQL Order By MySQL Delete MySQL Drop Table MySQL Update MySQL Limit MySQL Join

Python MongoDB

MongoDB 入门 MongoDB 创建数据库 MongoDB 创建集合 MongoDB 插入 MongoDB 查找 MongoDB 查询 MongoDB 排序 MongoDB 删除 MongoDB 删除集合 MongoDB 更新 MongoDB 限制

Python 参考手册

Python 参考手册 Python 内置函数 Python 字符串方法 Python 列表/数组方法 Python 字典方法 Python 元组方法 Python 集合方法 Python 文件方法 Python 关键字 Python 内置异常 Python 词汇表

Python 模块参考

Python 随机模块 Python 请求模块 Python 统计模块 Python 数学模块 Python cMath模块

Python 如何使用

Python 删除列表重复项 Python 反转字符串 Python 添加两个数字

Python 高级教程

Python 常用指引 将Python2代码迁移到Python3 将扩展模块移植到 Python3 Curses 编程 描述器使用指南 函数式编程指引 日志常用指引 日志操作手册 正则表达式使用指南 套接字编程指南 排序指南 Unicode 指南 如何利用urllib包获取网络资源 Argparse 教程 ipaddress 模块介绍 Argument Clinic 的用法 使用DTrace和SystemTap检测CPython 对象注解属性的最佳实践

Python 实例

Python 实例 Python 编译器 Python 练习 Python 测验 NumPy 测验 SciPy 测验

Python 对象注解属性的最佳实践

作者

Larry Hastings

摘要

本文意在汇聚对象的注解字典用法的最佳实践。 如果 Python 代码会去查看 Python 对象的 __annotations__ 属性,建议遵循以下准则。

本文分为四个部分:在 Python 3.10 以上版本中访问对象注解的最佳实践、在Python 3.9 以上版本中访问对象注解的最佳实践、适用于任何 Python 版本的其他 `__annotations__ 最佳实践、__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 开始,Python 函数、类和模块的 o.__annotations__ 保证 可用。如果确定是要查看这三种对象,只要利用 o.__annotations__ 读取对象的注释字典即可。

不过其他类型的可调用对象可能就没有定义 __annotations__ 属性,比如由 functools.partial() 创建的可调用对象。当访问某个未知对象的``__annotations__`` 时,Python 3.10 以上版本的最佳做法是带三个参数去调用 getattr() ,比如 getattr(o, '__annotations__', None)

Before Python 3.10, accessing __annotations__ on a class that defines no annotations but that has a parent class with annotations would return the parent's __annotations__. In Python 3.10 and newer, the child class's annotations will be an empty dict instead.


在 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__ 视作 globalsdict(vars(o)) 视作 locals

  • 如果 o 是一个用 functools.update_wrapper()functools.wraps()functools.partial() 封装的可调用对象,可酌情访问 o.__wrapped__o.func 进行反复解包,直到你找到未经封装的根函数。

  • 如果 o 是个可调用对象(但不是一个类),在调用 eval() 时,o.__dict__ 可视为 globals

但并不是所有注解字符串都可以通过 eval() 成功地转化为 Python 值。理论上,注解字符串中可以包含任何合法字符串,确实有一些类型提示的场合,需要用到特殊的 无法 被解析的字符串来作注解。比如:

  • 在 Python 3.10 加入支持之前,PEP 604 联合类型用到了 |

  • 运行时用不到的定义,只在 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'"}。这不应算是个“坑”;只是因为可能会让人吃惊,所以才提一下。