PEP 593 -- 函数或变量更灵活的注释方法

Meta

摘要

该 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 社区感兴趣的各种用例

这个问题最初是在 issue 600 中被提出,然后在 Python ideas 中进行了讨论

令人激动的例子

使用注解结合运行时和静态分析

库在运行时利用类型注释的趋势正在兴起(例如:数据类);能够使用外部数据扩展类型注释对于这些库来说将是一个巨大的福音。

下面是一个如何借助类型注释来读取 c 结构体的虚拟模块的示例:

1
2
3
4
5
6
7
8
9
10
11
12
UnsignedShort = Annotated[int, struct2.ctype('H')]
SignedChar = Annotation[int, struct2.ctype('b')]

class Student(struct2.Packed):
# mypy 静态检查 name 为 str 类型
name: Annotated[str, struct2.ctype("<10s")]
serialnum: UnsignedShort
school: SignedChar

# 'unpack' 仅使用类型注释中的元数据
Student.unpack(record)
# Student(name=b'raymond ', serialnum=4658, school=264)

降低开发新类型结构的成本

通常,在添加新类型时,开发者需要上传改类型到类型模块,并更改 mypy, Pycharm, Pyre, pytype 等,在处理这些类型的开源代码时,这一点尤为重要,如果没有额外的逻辑,这些无法立即传达给其他开发人员。因此在代码库中试图开发一个新类型的成本很高。理想情况下,作者应该能够允许以一种简单优雅的方式引入新的类型(例如:当客户端没有使用自定义的 mypy 插件时),这将降低开发成本并确保向后兼容性。

例如,假设作者想要在 Python 中添加一个联合标记的支持,方法是在 Python 中注解 TypeDict ,使其只支持一个字段被设置

1
2
3
4
Currency = Annotated[
TypedDict("Currency", {'dolars': 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] 无效)

  • 注释的顺序得到保留,并且相等性检查很严格

    1
    2
    Annotated[int, ValueRange(3, 10), ctype("char")] != Annotated[
    int, ctype("char"), ValueRange(3, 10)]
  • 嵌套的 Annotated 会被扁平化,元数据从最内层开始排序

    1
    2
    3
    Annotated[Annotated[int, ValueRange(3, 10)], ctype("char")] == Annotated[
    int, ValueRange(3, 10), ctype("char")
    ]
  • 重复的批注不会被删除

    1
    2
    3
    Annotated[int, ValueRange(3, 10), ValueRange(3, 10)] == Annotated[
    int, ValueRange(3, 10)
    ]
  • Annotated 可以与嵌套别名和通用别名一起使用

    1
    2
    3
    4
    Typevar T = ...
    Vec = Annotated[List[Tuple[T, T]], MaxLen(10)]
    V = Vec[int]
    V == Annotated[List[Tuple[int, int]], MaxLen(10)]

使用注解

归根结底,如何解释注释(如果存在)是由遇到的注释类型的工具或库的责任。一个工具或库可以在遇到注解类型时进行扫描注解来确定是否是值得关注的注解类型(例如:使用 isinstance() )。

未知注解:当工具或库遇到未知或不支持的注解时,应该选择忽略并将其注解类型视为基础类型,例如,在对 Annotated[str, 'foo', struct2.ctype("<10s")] 所注解的 name 不是 struct2.ctype 的情况下,应该将其忽略并注解为 str

命名空间注解:注释不需要命名空间,因为注释使用的类就是命名空间。

多个注解:由使用批注的工具决定是否允许客户端在一种类型上拥有多个批注以及如何合并这些批注。

由于 Annotated 类型运行你在任何节点放置多个相同或不同的类型,因此使用这些注释的工具或库负责处理潜在的重复项,例如,如果你正在进行值范围分析,你可以这样做

1
2
T1 = Annotated[int, ValueRange(-10, 5)]
T2 = Annotated[T1, ValueRange(-20, 3)]

扁平化嵌套注解意味着

1
T2 = Annotated[int, ValueRange(-10, 5), ValueRange(-20, 3)]

get_type_hints() 的交互

typing.get_type_hints() 将采用新的参数 include_extras 默认为 False 来保持向后兼容性,当 include_extras 值为 False 时,额外的返回值将从返回值中去除,否则,注释将原封不动的返回:

1
2
3
4
5
6
7
8
9
@strct2.packed
class Student(NamedTuple):
name: Annotated[str, struct2.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, struct2.ctype("<10s")]
}

别名和对冗长的关注

在多处频繁使用 Annotated 可能会很冗长;幸运的是,别名注解的能力意味着在实践中我们不期望用户编写大量样板代码

1
2
3
4
5
6
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] 过于相似),并且使构造函数无法传递 ( T(5) == C(5)C = Annotation[T, Ann] )。

为了保持设计简单,下面这个功能被省略了:

  • Annotated不能使用单个参数调用,Annotated 可以支持在使用单个参数调用时返回基础值,(例如:Annotated[int] == int )。这使规格复杂化,并且几乎没有好处的增加。

版权

本文已发布在公共网络上

翻译注释

  • de facto - 事实上的
  • encounters - 遇到\遭遇
  • leverage - 利用\借助
  • hypothetical - 假设的
  • degradation - 退化\降解
  • graceful - 优雅的
  • iron out the kinks - 消除缺陷
  • preserve backward compatibility - 保持向后兼容性