PEP 593 – 灵活的函数和变量注解
- 作者:
- Till Varoquaux <till at fb.com>, Konstantin Kashin <kkashin at fb.com>
- 发起人:
- Ivan Levkivskyi <levkivskyi at gmail.com>
- 讨论至:
- Typing-SIG 邮件列表
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 主题:
- 类型标注
- 创建日期:
- 2019年4月26日
- Python 版本:
- 3.9
- 发布历史:
- 2019年5月20日
摘要
本 PEP 引入了一种机制,用于通过任意元数据扩展 PEP 484 中的类型注解。
动机
PEP 484 为 PEP 3107 中引入的注解提供了标准的语义。PEP 484 具有规范性,但它是大多数注解消费者的事实标准;在许多静态检查的代码库中,类型注解被广泛使用,它们实际上排挤了任何其他形式的注解。PEP 3107 中描述的一些注解用例(数据库映射、外语桥接)鉴于类型注解的普及,目前尚不现实。此外,类型注解的标准化排除了仅受特定类型检查器支持的高级功能。
基本原理
本 PEP 向 typing 模块添加了一个 Annotated 类型,用于使用特定上下文的元数据装饰现有类型。具体来说,类型 T 可以通过类型提示 Annotated[T, x] 使用元数据 x 进行注解。此元数据可用于静态分析或在运行时。如果一个库(或工具)遇到类型提示 Annotated[T, x] 并且对元数据 x 没有特殊逻辑,它应该忽略它并简单地将类型视为 T。与 typing 模块中目前存在的 no_type_check 功能完全禁用函数或类的类型检查注解不同,Annotated 类型允许对 T 进行静态类型检查(例如,通过 mypy 或 Pyre,它们可以安全地忽略 x),同时在特定应用程序中运行时访问 x。引入此类型将解决 Python 社区感兴趣的各种用例。
这最初在 typing github 中作为 问题 600 被提出,然后又在 Python ideas 中讨论。
动机示例
结合注解的运行时和静态使用
越来越多的库在运行时利用类型注解(例如:dataclasses);能够用外部数据扩展类型注解对这些库来说将是一个巨大的福音。
以下是假想模块如何利用注解读取 C 结构体的示例
UnsignedShort = Annotated[int, struct2.ctype('H')]
SignedChar = Annotated[int, struct2.ctype('b')]
class Student(struct2.Packed):
# mypy typechecks 'name' field as 'str'
name: Annotated[str, struct2.ctype("<10s")]
serialnum: UnsignedShort
school: SignedChar
# 'unpack' only uses the metadata within the type annotations
Student.unpack(record)
# Student(name=b'raymond ', serialnum=4658, school=264)
降低开发新类型构造的门槛
通常,在添加新类型时,开发人员需要将该类型上游到 typing 模块并更改 mypy、PyCharm、Pyre、pytype 等……这在处理使用这些类型的开源代码时尤为重要,因为如果没有额外的逻辑,代码无法立即传输到其他开发人员的工具中。因此,在代码库中开发和尝试新类型的成本很高。理想情况下,作者应该能够以一种允许优雅降级的方式引入新类型(例如:当客户端没有自定义 mypy 插件时),这将降低开发门槛并确保一定程度的向后兼容性。
例如,假设作者想要为 Python 添加对 带标签的联合的支持。一种方法是注解 Python 中的 TypedDict,使其只允许设置一个字段
Currency = Annotated[
TypedDict('Currency', {'dollars': float, 'pounds': float}, total=False),
TaggedUnion,
]
这是一种有些繁琐的语法,但它允许我们迭代这个概念验证,并让那些尚不支持此功能的类型检查器(或其他工具)的用户在包含带标签联合的代码库中工作。作者可以轻松测试此提案并解决问题,然后再尝试将带标签联合上游到 typing、mypy 等。此外,那些不支持解析 TaggedUnion 注解的工具仍然能够将 Currency 视为 TypedDict,这仍然是一个近似值(略微不那么严格)。
规范
语法
Annotated 使用类型和任意 Python 值列表进行参数化,这些值表示注解。以下是语法的具体细节
Annotated的第一个参数必须是有效类型- 支持多个类型注解(
Annotated支持可变参数)Annotated[int, ValueRange(3, 10), ctype("char")]
Annotated必须至少使用两个参数调用(Annotated[int]无效)- 注解的顺序被保留,并且对相等性检查很重要
Annotated[int, ValueRange(3, 10), ctype("char")] != Annotated[ int, ctype("char"), ValueRange(3, 10) ]
- 嵌套的
Annotated类型会被展平,元数据从最内层注解开始排序Annotated[Annotated[int, ValueRange(3, 10)], ctype("char")] == Annotated[ int, ValueRange(3, 10), ctype("char") ]
- 重复的注解不会被删除
Annotated[int, ValueRange(3, 10)] != Annotated[ int, ValueRange(3, 10), ValueRange(3, 10) ]
Annotated可以与嵌套和泛型别名一起使用Typevar T = ... Vec = Annotated[List[Tuple[T, T]], MaxLen(10)] V = Vec[int] V == Annotated[List[Tuple[int, int]], MaxLen(10)]
消费注解
最终,如何解释注解(如果解释的话)的责任在于遇到 Annotated 类型的工具或库。遇到 Annotated 类型的工具或库可以扫描注解以确定它们是否感兴趣(例如,使用 isinstance())。
未知注解:当工具或库不支持注解或遇到未知注解时,它应该忽略它,并将注解类型视为底层类型。例如,当遇到不是 struct2.ctype 实例的注解(例如,Annotated[str, 'foo', struct2.ctype("<10s")])时,unpack 方法应该忽略它。
注解命名空间:注解不需要命名空间,因为注解使用的类充当命名空间。
多个注解:由消费注解的工具来决定客户端是否允许在一种类型上使用多个注解以及如何合并这些注解。
由于 Annotated 类型允许您在任何节点上放置多个相同(或不同)类型(或类型)的注解,因此消费这些注解的工具或库负责处理潜在的重复。例如,如果您正在进行值范围分析,您可能允许这样做
T1 = Annotated[int, ValueRange(-10, 5)]
T2 = Annotated[T1, ValueRange(-20, 3)]
展平嵌套注解,这转化为
T2 = Annotated[int, ValueRange(-10, 5), ValueRange(-20, 3)]
与 get_type_hints() 的交互
typing.get_type_hints() 将接受一个新参数 include_extras,其默认值为 False 以保持向后兼容性。当 include_extras 为 False 时,额外的注解将从返回值中剥离。否则,注解将不变地返回
@struct2.packed
class Student(NamedTuple):
name: Annotated[str, struct.ctype("<10s")]
get_type_hints(Student) == {'name': str}
get_type_hints(Student, include_extras=False) == {'name': str}
get_type_hints(Student, include_extras=True) == {
'name': Annotated[str, struct.ctype("<10s")]
}
别名和关于冗长的担忧
到处写 typing.Annotated 可能会非常冗长;幸运的是,别名注解的能力意味着实际上我们不期望客户端需要编写大量样板代码
T = TypeVar('T')
Const = Annotated[T, my_annotations.CONST]
class C:
def const_method(self: Const[List[int]]) -> int:
...
被拒绝的想法
本 PEP 拒绝了一些提议的想法,因为它们会导致 Annotated 无法与其他类型注解干净地集成
Annotated无法推断被装饰的类型。您可以想象Annotated[..., Immutable]可以用来将值标记为不可变,同时仍然推断其类型。Typing 不支持在任何其他地方使用推断类型;最好不要将其作为特殊情况添加。- 使用
(Type, Ann1, Ann2, ...)而不是Annotated[Type, Ann1, Ann2, ...]。这将在注解出现在嵌套位置时造成混淆(Callable[[A, B], C]与Callable[[(A, B)], C]太相似),并且将使构造函数无法直通(当C = Annotation[T, Ann]时,T(5) == C(5))。
为了保持设计简单,此功能被省略
Annotated不能使用单个参数调用。Annotated 可以支持在调用单个参数时返回底层值(例如:Annotated[int] == int)。这使得规范复杂化,并且几乎没有益处。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0593.rst