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中作为issue 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")]
),解包方法应忽略它。
**注解命名空间:**注解不需要命名空间,因为注解使用的类充当命名空间。
**多个注解:**使用注解的工具需要决定客户端是否允许在一个类型上有多个注解以及如何合并这些注解。
由于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]
可以用来将值标记为不可变,同时仍然推断其类型。类型提示不支持在任何其他地方使用推断的类型;最好不要将其添加为特殊情况。- 使用
(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