首页 > Python基础教程 >
-
PEP 484 类型提示 -- Python官方文档译文 [原创](5)
Text
的参数,str
和 unicode
类型也应该是可接受的。
未被接受的替代方案(Rejected Alternatives)
在讨论本 PEP 的早期草案时,出现过各种反对意见,并提出过一些替代方案。在此讨论其中一些意见,并解释一下未被接受的原因。
下面是几个主要的反对意见。
泛型参数该用什么括号?(Which brackets for generic type parameters?)
大多数人都熟知,在C++、Java、C# 和 Swift 等语言当中,用尖括号(如 List<int>
)来表示泛型的参数化。这种格式的问题是真的难以解析,尤其是对于像 Python 这种思维简单的解析器而言。在大多数语言中,通常只允许在特定的语法位置用尖括号来解决歧义,而这些位置不允许出现泛型表达式。并且还得采用非常强大的解析技术,可对任何一段代码进行重复解析(backtrack)。
但在 Python 中,更愿意让类型表达式(在语法上)与其他表达式一样,以便用变量赋值之类的操作就能创建类型别名。不妨看看下面这个简单的类型表达式:
List<int>
从 Python 解析程序的角度来看,以4个短语(名称、小于、名称、大于)开头的表达式将视为连续(chained)比较:
a < b > c # I.e., (a < b) and (b > c)
甚至可以创建一个两种解析方式共存的示例:
a < b > [ c ]
假设语言中包含尖括号,则以下两种解释都是可以的:
(a<b>)[c] # I.e., (a<b>).__getitem__(c)
a < b > ([c]) # I.e., (a < b) and (b > [c])
当然能够再提出一种规则来消除上述情况的歧义,但对于大多数用户而言,会觉得这些规则稍显随意和复杂。并且这还要将 CPython 解析程序(和其他所有Python 解析程序)做出很大的改动。有一点应该注意,Python 当前的解析程序是有意如此“愚蠢”的,这样用户很容易就能想到简单的语法。
因为上述所有原因,所以方括号(如 List[int]
)是(长期以来都是)泛型参数的首选语法。这通过在元类上定义 __getitem__()
方法就可以实现,根本不需要引入新的语法。这种方案在所有较新版本的 Python(从 Python 2.2 开始)中均有效。并非只有 Python 才选择这种语法,Scala 中的泛型类也采用了方括号。
已在用的注解怎么办(What about existing uses of annotations?)
有一条观点指出,PEP 3107 明确支持在函数注解中使用任意表达式。因此,本条新提案被认为与 PEP 3107 规范不兼容。
对此的回应是,首先本提案没有引入任何直接的不兼容性,因此使用注解的程序在 Python 3.4 中仍然可以正确运行,在 Python 3.5 中也毫无影响。
类型提示确实期望能最终成为注解的唯一用途,但这需要再多些讨论,而且 Python 3.5 才首次推出 typing
模块,也需要一段时间实现废弃(deprecation)。直至 Python 3.6 发布之前,当前的 PEP 都将为临时(provisional)状态(参阅 PEP 411)。可能的方案最快将自 3.6 开始无提示地废弃非类型提示的注解,自 3.7 开始完全废弃,并在 Python 3.8 中将类型提示声明为唯一允许使用的注解。即便类型提示在一夜之间取得了成功,也应该让带注解程序包的作者有足够的时间去更换方案。
更新:2017年秋季,本 PEP 和 type.py
模块的临时状态终止计划已作更改,因此其他注解用法的弃用计划也已更改。更新过的时间计划请参阅 PEP 563。
另一种可能的结果是,类型提示最终将成为注解的默认含义,但将其禁用的选项也会一直保留。为此,本提案定义了一个装饰器 @no_type_check
,该装饰器将禁止对给定类或函数中用作类型提示的注解作默认解释。这里还定义了一个元装饰器 @no_type_check_decorator
,可用于对装饰器进行装饰,使得用其装饰的任何函数或类中的注解都会被类型检查程序忽略。
且还有 # type: ignore
注解呢,静态检查程序应支持对选中包禁止类型检查的配置项。
尽管有这么多选择可用,但允许类型提示和其他形式的注解共存于参数中的提案已经发布过了一些。有一项提案建议,如果某个参数的注解是字典字面量,则每个字典键都表示一种不同格式的注解,字典键“type
”将用于类型提示。这种想法及其变体的问题在于,注解会变得非常“杂乱”,可读性会很差。而且大多数现有采用注解的库,几乎不需要与类型提示混合使用。因此,只要有选择地禁用类型提示就足够了。
前向声明的问题(The problem of forward declarations)
当类型提示必须包含向前引用时,当前提案无疑是次优选择。Python 要求所有变量在用到时再作定义。除了循环导入外,这不太会有问题:这里的“用到”表示“在运行时去作查找”,并且对于大多数“向前”引用而言,确保在用到名称的函数被调用之前定义名称就没有问题了。
类型提示的问题在于,在定义函数时会对注解进行求值(据 PEP 3107,类似于默认值),因此注解中使用的任何名称在定义函数时必须已经定义。常见的场景是类的定义,其方法需要在注解中引用类本身。更一般地说,在相互递归引用的类中也可能发生这种情况。对于容器类型而言这很自然,例如:
class Node:
"""Binary tree node."""
def __init__(self, left: Node, right: Node):
self.left = left
self.right = right
上述写法是行不通的,因为 Python 的特性就是,一旦类的全体代码执行完毕,类的名称就定义完成了。我们的解决方案不是特别优雅,但是可以完成任务,也就是允许在注解中使用字符串字面量。不过大多数时候都不必用到字符串,类型提示的大多数应用都应引用内置类型或其他模块中已定义的类型。
有一种答复将会修改类型提示的语义,以便根本不会在运行时对其进行求值。毕竟类型检查是脱机进行的,为什么要在运行时对类型提示进行求值呢。当然这与向下兼容有冲突,因为 Python 解释程序其实并不知道某个注解是要用作类型提示或是有其他用途。
有一种可行的折衷方案,就是用 __future__
导入可以将给定模块中的所有注解都转换为字符串字面量,如下所示:
from __future__ import annotations
class ImSet:
def add(self, a: ImSet) -> List[ImSet]: ...
assert ImSet.add.__annotations__ == {'a': 'ImSet', 'return': 'List[ImSet]'}
这种 __future__
导入语句可能会在单独的 PEP 中给出。
更新:PEP 563 中已讨论了 __future__
导入语句及其后果。
双冒号(The double colon)
一些有创造力的人已经尝试发明了多种解决方案。比如有人提议让类型提示采用双冒号(::),可以一次解决两个问题:消除类型提示与其他注解之间的歧义、修改语义避免运行时求值。但这种想法有以下几个问题。
- 难看(ugly)。在 Python 中单个冒号有很多用途,并且看起来都很熟悉,因为类似于英文中的冒号用法。这是一条普遍的经验法则,Python 会遵守标点符号的大多数使用格式,那些例外通常也是因其他编程语言而熟知的。但是 :: 的这种用法在英语中是闻所未闻的,而在其他语言(例如 C++)中是被用作作用域操作符的,这太与众不同了。相反,类型提示采用单个冒号读起来很自然,这不足为奇,因为这是为此目的而精心设计的(想法比 PEP 3107 gvr-artima 要早得多)。从 Pascal 到 Swift,其他很多语言也采用了相同的风格。
- 该如何处理返回类型的注解?
-
实际上这是在运行时对类型提示进行求值的特性。
- 让类型提示可用于运行时,使得能基于类型提示构建运行时的类型检查程序。
- 即便没有运行,类型检查程序仍能捕获错误。因为类型检查程序是一个单独的程序,所以用户可以选择不运行(甚至不安装),但仍可能想把类型提示用作简明的文档。错误的类型提示即便当作文档也没啥用处。
- 因为是新语法,所以把双冒号用于类型提示将会受到限制,只能适用于 Python 3.5 的代码。而利用现有的语法,本提案可以轻松应用于较低版本的Python 3。mypy 实际支持 Python 3.2 以上的版本。
-
如果类型提示获得成功,可能会决定在未来加入新的语法,用于声明变量的类型,比如
var age: int = 42
。如果参数的类型提示采用双冒号,那么为了保持一致,未来的语法必须采用同样的约定,如此难看的语法将会一直流传下去。
其他一些新语法格式(Other forms of new syntax)
还有一些其他格式的语法也被提出来过,比如引入 where
关键字,以及 Cobra-inspired requires
子句。但这些语法都和双冒号一样存在同样的问题,他们不适用于低版本的 Python 3。新的__future__ 导入语法也同样如此。
其他的向下兼容约定(Other backwards compatible conventions)
提出的想法有:
-
装饰器,比如
@typehints(name=str, returns=str)
。这可能会有用,但太啰嗦了(增加一行代码,并且参数名称必须重复一遍),且与 PEP 3107 的注解方式相去甚远。 - 存根文件。存根文件确实有需要,但主要是用来把类型提示加入已有代码中,这些代码本身不适合添加类型提示,例如:第三方软件包、需同时支持 Python 2和 Python 3 的代码、(特别是)扩展模块。在大多数情况下,与函数定义放在一起的行内注解会更加有用。
-
文档字符串。文档字符串对注解已有约定,即基于 Sphinx 注解方式
(:type arg1: description)
。这真有点啰嗦(每个参数增加一行代码),而且不太优雅。当然再创造一些新语法也是可以的,但很难超越注解语法(因为它是专为此目的而设计的)。
还有人提议,就坐等新的发行版本吧。但这能解决什么问题?只会是拖延下去罢了。
PEP 开发过程(PEP Development Process)
本 PEP 的最新文稿位于 GitHub 上。另有一个议题跟踪包含了很多技术讨论内容。
GitHub 上的文稿会定期进行小幅更新。通常正式的 PEPS 库只在新文稿发布到 python-dev 时才会更新。
致谢(Acknowledgements)
没有 Jim Baker、Jeremy Siek、Michael Matson Vitousek、Andrey Vlasovskikh、Radomir Dopieralski、Peter Ludemann 和BDFL-Delegate、Mark Shannon 的宝贵投入、鼓励和建议,本文就无法完成。
本文受到 PEP 482 中提及的现有语言、库和框架的影响。非常感谢其创建者(按字母序排列):Stefan Behnel、William Edwards、Greg Ewing、Larry Hastings、Anders Hejlsberg、Alok Menghrajani、Travis E. Oliphant、Joe Pamer、Raoul-Gabriel Urma、and Julien Verlaguet。