PEP 526 – 变量注解语法
- 作者:
- Ryan Gonzalez <rymg19 at gmail.com>, Philip House <phouse512 at gmail.com>, Ivan Levkivskyi <levkivskyi at gmail.com>, Lisa Roach <lisaroach14 at gmail.com>, Guido van Rossum <guido at python.org>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 主题:
- 类型标注
- 创建日期:
- 2016年8月9日
- Python 版本:
- 3.6
- 发布历史:
- 2016年8月30日,2016年9月2日
- 决议:
- Python-Dev 消息
状态
本PEP已被BDFL临时接受。有关更多细节,请参阅接受消息:https://mail.python.org/pipermail/python-dev/2016-September/146282.html
审阅者须知
本PEP是在一个单独的仓库中起草的:https://github.com/phouse512/peps/tree/pep-0526。
python-ideas 和 https://github.com/python/typing/issues/258 上进行了初步讨论。
在公共论坛提出异议之前,请至少阅读本PEP末尾列出的被拒绝的想法的摘要。
摘要
PEP 484引入了类型提示,也称为类型注解。虽然它的主要关注点是函数注解,但它也引入了通过类型注释来注解变量的概念。
# '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]
本PEP旨在为Python添加语法,用于注解变量(包括类变量和实例变量)的类型,而不是通过注释来表达。
primes: List[int] = []
captain: str # Note: no initial value!
class Starship:
stats: ClassVar[Dict[str, int]] = {}
PEP 484明确指出类型注释旨在帮助复杂情况下的类型推断,本PEP并未改变这一意图。然而,由于实际上类型注释也已被用于类变量和实例变量,本PEP也讨论了这些变量的类型注解的使用。
基本原理
尽管类型注释工作得很好,但它们通过注释来表达的事实有一些缺点。
- 文本编辑器通常将注释与类型注解区分开来。
- 无法注解未定义变量的类型;需要将其初始化为
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。 - 类型注释在typeshed中大量使用。将typeshed迁移到使用变量注解语法而不是类型注释将提高存根的可读性。
- 在普通注释和类型注释一起使用的情况下,很难区分它们。
path = None # type: Optional[str] # Path to module source
- 在运行时,除了尝试查找模块的源代码并进行解析之外,无法检索注解,这至少可以说是不优雅的。
这些问题中的大部分可以通过将语法作为语言的核心部分来缓解。此外,拥有专门用于类和实例变量(除了方法注解)的注解语法,将为静态鸭子类型作为PEP 484定义的标称类型的补充铺平道路。
非目标
尽管该提案伴随着 typing.get_type_hints 标准库函数的扩展,用于运行时检索注解,但变量注解并非设计用于运行时类型检查。必须开发第三方包才能实现此类功能。
还应强调的是,**Python 仍将是一种动态类型语言,作者无意使类型提示成为强制性的,即使是惯例也不行。** 类型注解不应与静态类型语言中的变量声明混淆。注解语法的目标是为第三方工具提供一种简单的方式来指定结构化的类型元数据。
本PEP不要求类型检查器更改其类型检查规则。它只是提供了一种更具可读性的语法来替代类型注释。
规范
类型注解可以添加到赋值语句或单个表达式中,以向第三方类型检查器指示注解目标的所需类型。
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中对合规性的态度一致。)
全局和局部变量注解
本地变量和全局变量的类型可以按如下方式注解。
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.
类和实例变量注解
类型注解也可以用于在类体和方法中注解类变量和实例变量。特别是,无值表示法 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
表达式注解
注解的目标可以是任何有效的单赋值目标,至少在语法上是这样(由类型检查器决定如何处理)。
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.
请注意,即使是加括号的名称也被视为表达式,而不是简单名称。
(x): int # Annotates x with int, (x) treated as expression by compiler.
(y): int = 0 # Same situation here.
不允许注解的情况
在同一个函数作用域内,尝试注解受 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:
...
存根文件中的变量注解
由于变量注解比类型注释更具可读性,因此在所有Python版本(包括Python 2.7)的存根文件中都首选它们。请注意,存根文件不被Python解释器执行,因此使用变量注解不会导致错误。类型检查器应支持所有Python版本存根中的变量注解。例如:
# file lib.pyi
ADDRESS: unicode = ...
class Error:
cause: Union[str, unicode]
变量注解的首选编码风格
模块级变量、类和实例变量以及局部变量的注解应在相应的冒号后有一个空格。冒号前不应有空格。如果赋值有右侧,则等号两侧应恰好有一个空格。示例:
- 是
code: int class Point: coords: Tuple[int, int] label: str = '<unknown>'
- 否
code:int # No space after colon code : int # Space before colon class Test: result: int=0 # No spaces around equality sign
标准库和文档的变更
typing模块中新增了一个协变类型ClassVar[T_co]。它只接受一个参数,该参数应为有效类型,用于注解不应在类实例上设置的类变量。此限制由静态检查器确保,但在运行时不生效。有关ClassVar用法的示例和解释,请参阅classvar部分;有关ClassVar背后推理的更多信息,请参阅rejected部分。typing模块中的get_type_hints函数将得到扩展,以便在运行时可以从模块、类以及函数中检索类型注解。注解以字典形式返回,将变量或参数映射到其类型提示,并评估前向引用。对于类,它返回一个根据方法解析顺序构建的映射(可能是collections.ChainMap)。- 使用注解的建议指南将添加到文档中,其中包含本 PEP 和 PEP 484 中描述的规范的教学式总结。此外,一个用于将类型注释转换为类型注解的辅助脚本将独立于标准库发布。
类型注解的运行时效应
注解局部变量将使解释器将其视为局部变量,即使从未给它赋值。局部变量的注解将不被评估。
def f():
x: NonexistentName # No error.
然而,如果它在模块或类级别,那么类型将会被评估。
x: NonexistentName # Error!
class X:
var: NonexistentName # Error!
此外,在模块或类级别,如果被注解的项是简单名称,则它和注解将作为从名称到评估注解的有序映射存储在该模块或类的__annotations__属性中(如果私有则混淆)。示例如下:
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 函数;与所有双下划线属性一样,任何未经文档记录的 __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__ 字典。此外,局部可用的注解的价值并不足以抵消每次函数调用时创建和填充注解字典的成本。因此,函数级别的注解不被评估和存储。
注解的其他用途
虽然带有此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 中,我们明确建议将类型提示作为注解的首选用途。
被拒绝/推迟的提议
- **我们是否应该引入变量注解?** 变量注解已经以类型注释的形式存在了将近两年,并得到了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 直接对此说了“不”。 - **也将
InstanceVar添加到 typing 模块中:** 这是多余的,因为实例变量比类变量更常见。更常见的用法应该成为默认值。 - **仅允许在方法中进行实例变量注解:** 问题是许多
__init__方法除了初始化实例变量之外还会做很多事情,而且(对人来说)更难找到所有实例变量注解。有时__init__被分解成更多的辅助方法,所以追踪它们甚至更难。将实例变量注解放在类中更容易找到它们,并有助于代码的初次读者。 - **对于类变量,使用语法
x: class t = v:** 这将需要一个更复杂的解析器,并且class关键字会使简单的语法高亮器感到困惑。无论如何,我们需要ClassVar将类变量存储到__annotations__中,因此选择了更简单的语法。 - **完全忘记
ClassVar:** 曾有人提出此建议,因为 mypy 似乎在没有办法区分类变量和实例变量的情况下运行良好。但是,类型检查器可以利用这些额外信息做一些有用的事情,例如标记通过实例对类变量的意外赋值(这会创建一个遮蔽类变量的实例变量)。它还可以标记带有可变默认值的实例变量,这是一个众所周知的危险。 - **使用
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,只要它能证明它肯定会被定义。)
向后兼容性
本PEP完全向后兼容。
实施
Python 3.6 的实现可在 GitHub 上找到。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0526.rst