PEP 3107 -- 函数注解

Meta

摘要

该 PEP 引入了一个为 Python 的函数 [1] (可调用对象)添加任意元数据注解的语法

理由

由于 Python 2.x 缺少一个对函数参数和返回值注解的标准语法,各种工具或库的出现填补了这一空白,有些利用了 PEP 318 中引入的装饰器,还有一些则通过解析函数的文档注释,寻找其中的注释来实现。

本 PEP 的目的是为这些信息提供一个唯一、标准的方法,减少由于机制和语法的巨大差异而造成的困惑

函数注释基础

在开始讨论 Python3.0 函数注解的确切来龙去脉之前,我们先从管以上谈谈什么是注解,什么不是注解:

  1. 函数注解,无论是参数还是返回值,都是可选的

  2. 函数注解只不过是在编译时将独立的 Python 表达式与函数任意部分关联起来的一种方式

    就其本身而言,Python 并没有为注解添加任何特定的含义或意义,就起本身而言,Python 只是单纯的让下文中访问函数注解的表达式可用。

    注解具有的唯一意义是能够被第三方库所解释。注解的使用者能够用这些函数注解作任何它们想做的事情。例如,一个库可能使用基于字符串的注解来提供帮助信息。如下所示:

    1
    2
    3
    4
    def compile(source: "something compilable",
    filename: "where the compilable thing comes from",
    mode: "is this a single statement or a suite?"):
    ...

    另一个库可能用来对 Python 的函数或方法提供类型检查,这个库能够使用注解来提示期待的输入和输出的类型,可能像下面这样:

    1
    2
    def hula(item: Hualable, *vargs: PackAnimal) -> Distance:
    ...

    然后,无论是第一个示例中的字符串,还是第二个示例中的类型信息本身都没有任何含义,这些含义仅取决于三方库

  3. 基于第 2 点,这个 PEP 没有尝试引入任何标准的语法,即使是内置类型也是如此,这项工作将留给第三方库

语法

参数

参数的注解采用跟在参数后面的可选表达式的形式:

1
2
def foo(a: expression, b: expression = 5):
...

在伪语法中,参数现在形如 identifier [: expression] [= expression] 。也就是说,注解始终位于默认值的前面,并且注解和默认值都是可选的。就像用等号表示默认值一样,冒号用来标识注解。所有的注解表达式都会在函数定义时被执行,就像默认值表达式一样。

多余参数 (如:*args**kwargs) 的注解也以类似的方式标识:

1
2
def foo(*args: expression, **kwargs: expression):
...

嵌套参数的注解始终跟在参数名称后面,而不是最后一个括号后面,不需要对签到参数的所有参数进行注解:

1
2
3
def foo((x1, y1: expression),
(x2: expression, y2: expression)=(None, None)):
...

返回值

这些示例到目前为止忽略了函数返回值类型注解。具体做法如下:

1
2
def sum() -> expression:
...

也就是说,现在参数列表后面可以跟随一个字面的 -> 符号和一个 Python 的表达式,与参数注解一样,这个表达式将会在函数定义的时候被执行。

现在函数定义 [11] 的语法是:

1
2
3
4
5
6
7
8
9
10
11
decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE
decorators: decorator+
funcdef: [decorators] 'def' NAME parameters ['->' test] ':' suite
parameters: '(' [typedargslist] ')'
typedargslist: ((tfpdef ['=' test] ',')*
('*' [tname] (',' tname ['=' test])* [',' '**' tname]
| '**' tname)
| tfpdef ['=' test] (',' tfpdef ['=' test])* [','])
tname: NAME [':' test]
tfpdef: tname | '(' tfplist ')'
tfplist: tfpdef (',' tfpdef)* [',']

匿名函数

lambda 的语法不支持注解,可以改变 lambda 的语法,要求在参数列表周围加上括号来支持注解,但是,决定 [12] 不进行这个修改,因为:

  • 这是一个不兼容的修改
  • lambda 总是被精简过的
  • lambda 总是可以修改为一个函数

访问函数注解

编译后,一个函数的注解始终可以通过函数的 __annotations__ 属性来获得。这个属性是一个可变字典,参数名称将会映射到一个注解表达式执行的对象上。

__annotations__ 映射中有一个特殊的键 return 。这个键只有为函数返回值提供注解的时候才会存在。

例如,存在一下注解:

1
2
def foo(a: 'x', b: 5 + 6, c: list) -> max(2, 9):
...

那么,__annotations__ 将会是:

1
2
3
4
5
6
{
'a': 'x',
'b': 11,
'c': list,
'return': 9
}

之所以选择 return 作为键,是因为它不会与参数名称冲突,任何尝试将 return 作为参数名称的函数都将报 SyntaxError 的错误。

如果函数没有任何注解或者函数从 lambda 创建的,那么 __annotations__ 会是一个空的可变字典

使用案例

在讨论注解的过程中,有一些案例被提出来了。这里介绍了一些,它们按照传达的信息进行了分类。还包含了一些使用了注解的现有的产品或包的示例。

  • 提供类型信息
    • 类型检查 ([3], [4])
    • 帮助 IDE 显示函数期望的参数和返回值类型 ([16])
    • 函数重载/泛型函数 ([21])
    • 外语桥接 ([17], [18])
    • 改写 ([19], [20])
    • 谓词逻辑函数
    • 数据库查询映射
    • RPC 参数封装 ([22])
  • 其他信息
    • 参数和返回值文档 ([23])

标准库

pydoc 和 inspect

pydoc 模块应该在显示函数帮助的时候同时显示函数的注解

inspect 模块应该进行修改来支持注解

关联的其他 PEP

函数签名对象(PEP 362)

函数签名对象应该展示函数的注解,Parameter 对象可能需要更改或者其他的更改来保证该展示

实现

参考实现已经在 py3k (前身为 p3yk) 修改分支 53130 [10] 中完成

被拒绝的提案

  • BDFL(译者注: 指被称为 Python 独裁者的 Guido Van Rossum) 以 “太难看了” 拒绝了作者关于为生成器添加注解的特殊语法的想法[2]
  • 尽管在早期讨论过 ([5], [6]),但是在 stdlib 中关于生成器和高阶函数的特殊对象注解最终被拒绝,因为它更适合第三方库,将它们包含在标准库中会引发更多棘手的问题。
  • 尽管对标准类型参数化语法进行了大量的讨论,最后还是决定将其留给第三方库。([7], [8], [9])。
  • 尽管进行了更多的讨论,但是还是决定不对注释互操作性进制进行标准化。在这个时候将互操作性约定标准化为时尚早。我们宁可让这些惯例根据实际使用情况和需要有机的发展起来,而不是试图强迫所有用户采用某种设计好的方案。 ([13], [14], [15])

参考文献和注释

版权

本文已发布在公共网络上

翻译词典

  • introduced - 引入
  • wide - 大
  • variation - 更改,变异
  • mechanism - 机制