PEP 526 变量注解的语法 -- Python官方文档译文 [原创]
PEP 526 -- 变量注解的语法(Syntax for Variable Annotations)
英文原文:https://www.python.org/dev/peps/pep-0526
采集日期:2020-02-27
PEP: 526
Title: Syntax for Variable Annotations
Author: Ryan Gonzalez rymg19@gmail.com, Philip House phouse512@gmail.com, Ivan Levkivskyi levkivskyi@gmail.com, Lisa Roach lisaroach14@gmail.com, Guido van Rossum guido@python.org
Status: Final
Type: Standards Track
Created: 09-Aug-2016
Python-Version: 3.6
Post-History: 30-Aug-2016, 02-Sep-2016
Resolution: https://mail.python.org/pipermail/python-dev/2016-September/146282.html
目录
- 重要程度(Status)
- 评论者的注意事项(Notice for Reviewers)
- 摘要(Abstract)
- 原由(Rationale)
- 规范(Specification)
- 标准库和文档的改动(Changes to Standard Library and Documentation)
- 类型注解的运行时效果(Runtime Effects of Type Annotations)
- 被拒绝/搁置的提案(Rejected/Postponed Proposals)
- 向下兼容性(Backwards Compatibility)
- 实现代码(Implementation)
- 版权(Copyright)
重要程度(Status)
本 PEP 暂时已被 BDFL 收录。更多观点请参阅收录信息。
评论者的注意事项(Notice for Reviewers)
本 PEP 是在单独的 repo 中起草的:https://github.com/phouse512/peps/tree/pep-0526。
初步的讨论位于 python-ideas 和 https://github.com/python/typing/issues/258 上。
若要在公共论坛上提出异议,至少请先阅读一下本 PEP 最后列出的被拒绝提议的主要内容。
摘要(Abstract)
PEP 484 引入了类型提示(type hint),又称类型注解(type annotation)。尽管其重点是函数注解,但也引入了类型注释(type comment)的概念用于注解变量:
# 'primes' is a list of integers
primes = [] # type: List[int]
# 'captain' is a string (Note: initial value is a problem)
captain = ... # type: str
class Starship:
# 'stats' is a class variable
stats = {} # type: Dict[str, int]
本文旨在为 Python 添加一种语法,用于对变量(包括类变量和实例变量)的类型做出注解,以取代通过注释(comment)来表达类型的方式:
primes: List[int] = []
captain: str # Note: no initial value!
class Starship:
stats: ClassVar[Dict[str, int]] = {}
PEP 484 明确指出类型注释旨在帮助复杂情况下的类型推断,本 PEP 不会改变此意图。但实际情况是类变量和实例变量也用到了类型注释,因此本 PEP 还讨论了为这些变量添加类型注解的用法。
原由(Rationale)
尽管类型注释已经够用了,但也表现出一些缺点:
-
文本编辑器经常会将注释高亮显示为类型注解不同的方式。
-
无法为未定义变量添加类型注释,需将其初始化为
None
(例如a = None # type: int
) -
条件分支语句内的变量注释可读性不好:
if some_value:
my_var = function() # type: Logger
else:
my_var = another_function() # Why isn't there a type here?
-
因为类型注释其实不是语言的组成部分,如果 Python 代码要对其进行解析,就需要自定义解析程序,而不能只用
ast
(Abstract Syntax Tree,抽象语法树) 解决。 -
类型注释已大量应用于 typeshed 中。将 typeshed 迁移为采用变量注解的语法,替换掉类型注释,可以提高存根文件的可读性。
-
在混合使用普通注释和类型注释的场合,要做出区分是比较困难的:
path = None # type: Optional[str] # Path to module source
- 除了尝试查看模块的源代码并在运行时进行解析,就再无他法在运行时读取注解信息了。至少可以认为这种做法不够优雅。
通过让注解语法成为语言的核心内容,可以缓解上述大多数问题。此外,作为由 PEP 484 定义的名称定型(nominal typing)的补充,专用于类和实例变量(方法注解)的注解语法将为静态鸭子定型铺平道路,
非本文目标(Non-goals)
虽然本提案和用于运行时读取注解信息的标准库函数 typing.get_type_hints
扩展一起出现,但变量注解并不是为运行时类型检查而设计的。必须开发第三方软件包才能实现该类型检查功能。
还应该强调的是,**Python 仍将是一种动态定型语言,并且按惯例作者不希望让类型提示成为强制要求。类型注解不应与静态定型语言中的变量声明相混淆。注解语法旨在为第三方工具提供一种简便的方法,用于定义结构化类型的元数据。
本 PEP 不需要类型检查程序改变其类型检查规则。这里只是提供了一种可读性更好的语法,以便替换类型注释。
规范(Specification)
可以在一条赋值语句或某个表达式中加入类型注解,以向第三方类型检查程序标示出被注解对象的应有类型:
my_var: int
my_var = 5 # Passes type check.
other_var: int = 'a' # Flagged as error by type checker,
# but OK at runtime.
上述表达式并没有引入超过 PEP 484 范围的新语义,因此以下三条语句是等效的:
var = value # type: annotation
var: annotation; var = value
var: annotation = value
下面给出各种上下文环境中的类型注解语法定义,以及运行时的效果。
同时给出了类型检查程序对注释的解析建议,但这些建议不是必须遵守的。这符合 PEP 484 对合规性的态度。
全局和局部变量的注解(Global and local variable annotations)
局部和全局变量的类型可以如下做出注解:
some_number: int # variable without initial value
some_list: List[int] = [] # variable with initial value
省略初始值能让条件分支语句中的变量更容易定型:
sane_world: bool
if 2+2 == 4:
sane_world = True
else:
sane_world = False
注意,尽管语法上确实允许元组打包时带上注解,但在采用元组解包写法时不允许注解变量的类型。
# Tuple packing with variable annotation syntax
t: Tuple[int, ...] = (1, 2, 3)
# or
t: Tuple[int, ...] = 1, 2, 3 # This only works in Python 3.8+
# Tuple unpacking with variable annotation syntax
header: str
kind: int
body: Optional[List[str]]
header, kind, body = message
若省略初始值,则变量为未初始化状态:
a: int
print(a) # raises NameError
如果给局部变量加上注解,则会让解释器一直将其视为局部变量:
def f():
a: int
print(a) # raises UnboundLocalError
# Commenting out the a: int makes it a NameError.
以下代码也是一样:
def f():
if False: a = 0
print(a) # raises UnboundLocalError
重复的类型注解将被忽略。但静态类型检查程序可以发出一条警告信息,提示同一个变量注解为不同类型:
a: int
a: str # Static type checker may or may not warn about this.
类和实例变量的注解(Class and instance variable annotations)
类型注解也可在类和方法内部用于为类和实例变量加上注解。特别是 a: int
这种不给出值的注解,使得应在 __init__
或 __new__
中进行初始化的实例变量也能加上注解。建议语法如下:
class BasicStarship:
captain: str = 'Picard' # instance variable with default
damage: int # instance variable without default
stats: ClassVar[Dict[str, int]] = {} # class variable
以上的 ClassVar
是一个由 typing 模块定义的特殊类,向静态类型检查程序标示在实例中不允许对该变量进行赋值。
请注意,无论嵌套多少层,ClassVar
的参数中都不能包含任何类型变量:如果 T
是类型变量的话,ClassVar[T]
和 ClassVar[List[Set[T]]]
都是非法的。
用个更详细的例子来演示一下吧。
class Starship:
captain = 'Picard'
stats = {}
def __init__(self, damage, captain=None):
self.damage = damage
if captain:
self.captain = captain # Else keep the default
def hit(self):
Starship.stats['hits'] = Starship.stats.get('hits', 0) + 1
在以上类中,stats
应该是一个类变量(用于记录每局游戏的各种状态),而 captain
则是一个默认值由类设置的实例变量。类型检查程序可能发现不了这两者的差异:两者都在类中进行了初始化,但 captain
仅作为便于实例变量使用的默认值,而 stats
则真是打算让所有实例共享的类变量。
由于两个变量恰好都在类这个级别进行了初始化,因此将类变量标记为以 ClassVar[...]
包裹的类型注释,对区分他们是很有用的。这样若对实例中同名属性发生意外赋值,类型检查程序就可以做出标记。
比如对上述类加上以下注解:
class Starship:
captain: str = 'Picard'
damage: int
stats: ClassVar[Dict[str, int]] = {}
def __init__(self, damage: int, captain: str = None):
self.damage = damage
if captain:
self.captain = captain # Else keep the default
def hit(self):
Starship.stats['hits'] = Starship.stats.get('hits', 0) + 1
enterprise_d = Starship(3000)
enterprise_d.stats = {} # Flagged as error by a type checker
Starship.stats = {} # This is OK
为了方便使用和遵循惯例,实例变量可以在 __init__
或其他方法中进行注解,而不是在类中进行:
from typing import Generic, TypeVar
T = TypeVar('T')
class Box(Generic[T]):
def __init__(self, content):
self.content: T = content
表达式的注解(Annotating expressions)
注解的对象可以是任一合法的可赋值物,至少在语法上是如此的(视类型检查程序采取的对策而定):
class Cls:
pass
c = Cls()
c.x: int = 0 # Annotates c.x with int.
c.y: int # Annotates c.y with int.
d = {}
d['a']: int = 0 # Annotates d['a'] with int.
d['b']: int # Annotates d['b'] with int.
请注意,虽然带括号的变量名也被视为表达式,但其不是简单名称(simple name):
(x): int # Annotates x with int, (x) treated as expression by compiler.
(y): int = 0 # Same situation here.
注解允许出现的位置(Where annotations aren't allowed)
在函数同级作用域内将变量注解为 global
或 nonlocal
是非法操作。
def f():
global x: int # SyntaxError
def g():
x: int # Also a SyntaxError
global x
原因就是这些变量并不归属于 global
或 nonlocal
,因此类型注解归属于拥有变量的作用域。
只允许存在一个赋值对象和一个右值。此外,不能对 for
或 with
语句中用到的变量进行注解,可以像元组解包那样提前做出注解:
a: int
for a in my_iter:
...
f: MyFile
with myfunc() as f:
...
存根文件中的变量注解(Variable annotations in stub files)
因为变量注解的可读性比类型注释更好,所以推荐所有版本 Python(包括 Python 2.7)的存根文件使用。请注意,Python 解释器不会执行存根文件,因此变量注解不会引发报错。类型检查程序应当支持所有版本 Python 存根文件中的变量注解。例如:
# file lib.pyi
ADDRESS: unicode = ...
class Error:
cause: Union[str, unicode]
变量注解的推荐编码风格(Preferred coding style for variable annotations)
对于模块级变量、类与实例变量、局部变量,类型注解的冒号后面应带一个空格。冒号前则不应有空格。如果赋值有右值,则等号两边都应带有一个空格。例如:
- Yes::
code: int
class Point:
coords: Tuple[int, int]
label: str = '<unknown>'
- No::
code:int # No space after colon
code : int # Space before colon
class Test:
result: int=0 # No spaces around equality sign
标准库和文档的改动(Changes to Standard Library and Documentation)
-
typing
中已新加入一个协变类型ClassVar[T_co]
。它只接受一个参数,应为一个合法的类型,它应用于不允许在类实例中赋值的类变量。这一约束由静态检查程序来保证,而不是运行时。有关ClassVar
用法的示例和说明,请参阅类和实例变量的注解部分;有关实施ClassVar
背后原因的更多信息,请参见被拒绝/搁置的提案部分。 -
typing
模块中的函数get_type_hints
将会作出扩展,以便在运行时可由模块、类和函数中读取类型注解。注解以字典映射的形式返回,由变量或参数映射为类型提示,若有向前引用则会先解析求值(evaluate)。如果是类,则按方法的解析顺序返回由注解构造的映射(或许是个collections.ChainMap
)。 -
文档中将会加入注解的推荐使用指南,包括本 PEP 和 PEP 484 所介绍规范的内容摘要。此外,还将发布一款将类型注释转换为类型注解的助手代码,其将与标准库分开发布。
类型注解的运行时效果(Runtime Effects of Type Annotations)
即便某本地变量从未赋值,只要对其添加了注解,解释器就将视其为本地变量。本地变量的注解不会被解析求值。
def f():
x: NonexistentName # No error.
但如果变量是模块或类级别的,则类型注解会被解析求值。
x: NonexistentName # Error!
class X:
var: NonexistentName # Error!
此外在模块或类级别,如果被注解对象是简单名称,则将其和注解一起存放于模块或类的 __annotations__
属性中,若为私有变量则信息会不全(mangle),形式为名称和已解析注释的有序字典。示例如下。
from typing import Dict
class Player:
...
players: Dict[str, Player]
__points: int
print(__annotations__)
# prints: {'players': typing.Dict[str, __main__.Player],
# '_Player__points': <class 'int'>}
__annotations__
是可写入属性,因此以下操作是允许执行的:
__annotations__['s'] = str
但如果试图将 __annotations__
修改为有序映射之外的其他对象,则可能会引发 TypeError:
class C:
__annotations__ = 42
x: int = 5 # raises TypeError
注意:给 __annotations__
赋值是 Python 解释器允许的操作,它不会过问。但随后的类型注解应该是 MutableMapping
类型,于是才会失败。
在运行时读取注解的推荐方式是采用 typing.get_type_hints
函数。与所有双下划线(dunder)属性一样,任何未在文档注明的对 __annotations__
的使用都难免失败,且不会发出警告:
from typing import Dict, ClassVar, get_type_hints
class Starship:
hitpoints: int = 50
stats: ClassVar[Dict[str, int]] = {}
shield: int = 100
captain: str
def __init__(self, captain: str) -> None:
...
assert get_type_hints(Starship) == {'hitpoints': int,
'stats': ClassVar[Dict[str, int]],
'shield': int,
'captain': str}
assert get_type_hints(Starship.__init__) == {'captain': str,
'return': None}
请注意,如果静态检查没有找到注解信息,则 __annotations__
字典根本不会被创建。而且本地存储注解获得的好处,并不能抵消每次函数调用时都得创建并填充注解字典的开销。因此,对函数级别的注解不会作解析求值和存储。
注解的其他用途(Other uses of annotations)
因为 Python 并不在意类型注解的存在,而不是“未经加载即作解析求值”,所以支持本 PEP 的 Python 不会拒绝以下形式:
alice: 'well done' = 'A+'
bob: 'what a shame' = 'F-'
除非用 # type: ignore
或 @no_type_check
进行了禁用,否则类型检查程序在读到注解时就会做出标记。
但正因为 Python 不在乎什么“类型”,所以如果以上代码段是全局级别或位于某个类中,则__annotations__
将会包含 {'alice': 'well done', 'bob': 'what a shame'}
。
这些存储下来的注解可用作其他用途,但本 PEP 明确推荐将类型提示作为注解的首选用途。
被拒绝/搁置的提案(Rejected/Postponed Proposals)
-
是否该引入变量注解?
变量注解已经以类型注释(comment)的形式存在了将近两年,PEP 484 也已认可。在第三方类型检查程序(mypy、pytype、PyCharm等)和运用类型检查程序的项目中,注解已得以广泛应用。但是注释语法存在着很多缺点,这在原由部分已有列出。本 PEP 并不讨论类型注解的必要性,而是介绍这种注解的语法。 -
引入新的关键字:
首先,要选出好的关键字非常困难。比如不能为var
,因为这在变量名中太常见了。如果要用于类变量或全局变量,则也不能为local
。其次,无论选择什么,仍需要用到__future__
导入语句。 -
用
def
作为关键字:
这种提案可能如下所示:
def primes: List[int] = []
def captain: str
这里的问题是,对于几代 Python 程序员(和工具!)而言,def
都表示“定义一个函数”,用它同时定义变量并不会增加清晰度。(尽管这确实是主观意见。)
-
用语法表明函数的用意:
此条提案建议用var = cast(annotation[, value])
注释变量类型。尽管这种语法缓解了类型注释的某些问题(如 AST 中没有注解),但其他问题还是没有解决(如高可读性、可能引入运行时开销)。 -
元组解包格式中允许加入类型注解:
这会导致歧义。以下语句的含义就不明:
x, y: T
x
和 y
都是 T
类型?或者 T
是由 x
和 y
得来的元组类型?或者 x
的类型为 Any
而 y
的类型为 T
?(如果出现在函数签名中,则就是这个意思。)至少到目前为止禁止如此,不能让人去猜。
-
注解采用括号形式
(var: type)
:
这是为解决上述歧义而在 python-ideas 上提出的,但语法啰嗦、好处不多且可读性差,因此被拒绝。 -
在连续赋值语句中允许使用注解:
与元组解包格式类似,这存在歧义和可读性问题。比如:
x: int = y = 1
z = w: int = 1
这里就存在歧义,y
和 z
应该是什么类型呢?而且第二行还难以作语法解析。
-
在
with
和for
语句中允许使用注解:
因为这样在for
语句中会让真正的迭代过程难以被发现,而在with
语句中则会引起 CPython 的 LL(1) 语法分析程序发生混乱。 -
在函数定义阶段对本地注解进行解析估值:
Guido 已拒绝此提案,因为注解的位置强烈表明其位于周围代码的相同作用域内。 -
在函数作用域内存储变量注解:
注解可本地访问的收益不足以显著抵消每次函数调用时创建和填充字典的开销。 -
对带有注解的变量未经赋值即进行初始化
有人在 python-ideas 上建议,将x: int
中的x
初始化为None
或其他特殊常量(类似 Javascript 的undefined
)。但是,在语言中新增一个单例值需要代码处处做出判断。因此,Guido 干脆地予以拒绝。 -
在 typing 模块中也加入
InstanceVar
:
纯属多余。因为实例变量比类变量更为常用。常用用法理应默认。 -
仅允许在方法内对实例变量进行注解:
问题在于,除了初始化实例变量之外,许多__init__
方法还会干很多活儿,而且人眼很难找齐所有实例变量的注释。有时__init__
会分解为更多助手方法,因此注释就更加难以追踪了。将实例变量的注释放到类中,找起来可以更加轻松,也会给第一次阅读代码的人带来便利。 -
类型变量的注释采用
x: class t = v
的语法:
这样会要求语法解析器变得更为复杂,class
关键字也会把简单的语法高亮显示程序弄糊涂。无论如何,都需要ClassVar
把类变量存储到__annotations__
中去,因此就选用了更简单的语法。 -
完全不用
ClassVar
:
mypy 无法区分类变量和实例变量,可貌似也能混的不错,因此才会有这个提案。但是类型检查程序利用这些附加信息能够干很多有用的工作,比如标记出由实例对类变量的意外赋值。这种赋值会创建实例变量,将类变量遮掩起来(shadow)。类型检查程序还可以将实例变量标记为默认可修改,声明众所周知的风险。 -
用
ClassAttr
替换ClassVar
:
ClassVar 更为合适,主要是因为类的属性可以有很多,如方法、描述符等。但是从概念上讲,只有特定的属性才是类变量(或常量)。 -
不对注解进行解析求值,只视其为字符串:
对函数注解始终都会进行解析求值,这样就会与其表现不一。尽管未来可能会重新考虑,但在 PEP 484 中已决定应该将其作为单独的 PEP 进行规范。 -
在类的文档字符串中对变量类型进行注解
许多项目已经应用了各种文档字符串规范,一致性往往不太好,通常还不符合 PEP 484 的注解语法。并且也还是需要比较复杂的特殊语法解析器。本 PEP 的目标正是要与第三方类型检查工具协作,如此目标就会落空。 -
将
__annotations__
实现为描述符
这条提案是为了禁止将__annotations__
设为除字典和None
之外的东西。Guido 拒绝了这个提案,认为没有必要。如果试图将__annotations__
修改为字典映射之外的任何东西,都会引发 TypeError。 -
将纯注解视同全局或非局部的:
这条提案希望,出现在函数体内的无赋值注解不应进行任何解析求值。与之相反,本 PEP 表明,如果注解目标比单个名称复杂,则应在函数体内的目标出现位置对其“左值部分”进行解析求值,以强制确认其是否已经定义。例如在以下示例中:
def foo(self):
slef.name: str
slef
就应该被解析求值,这样若是其尚未定义(本例中貌似就是如此:-)),运行时将会引发错误。这样就与带初值时的表现更为一致,因此应该能减少意外情况的发生。还有一点请注意,如果注解目标是 self.name
(这次拼写正确了:-)),那么做过优化的编译器并不保证会对 self
进行解析求值,只要能够证明其一定是已定义的即可。
向下兼容性(Backwards Compatibility)
本 PEP 完全向下兼容。
实现代码(Implementation)
适用于 Python 3.6 的已实现代码可在以下 GitHub repo 中找到:https://github.com/ilevkivskyi/cpython/tree/pep-526。
版权(Copyright)
本文已在公共领域发布。