Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 增强提案

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 是一份历史文档:请参阅 带注解的赋值语句ClassVartyping.ClassVar 以获取最新的规范和文档。规范的类型规范在 类型规范站点 中维护;运行时类型行为在 CPython 文档中进行了描述。

×

有关如何提出对类型规范的更改,请参阅 类型规范更新流程

状态

此 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.

不允许注解的位置

尝试注解在同一函数作用域中受 globalnonlocal 约束的变量是非法的

def f():
    global x: int  # SyntaxError

def g():
    x: int  # Also a SyntaxError
    global x

原因是 globalnonlocal 不拥有变量;因此,类型注解属于拥有变量的作用域。

仅允许单个赋值目标和单个右侧值。此外,不能注解在 forwith 语句中使用的变量;它们可以提前注解,类似于元组解包

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

(请注意,Python 解释器在不质疑的情况下接受对 __annotations__ 的赋值(这是罪魁祸首),但后续的类型注释期望它是一个 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__ 字典。此外,在本地使用注释的价值并不能抵消在每次函数调用时都必须创建和填充注释字典的成本。因此,函数级别的注释不会被评估,也不会被存储。

注解的其他用途

虽然使用此 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
    

    xy 的类型都是 T,还是我们期望 T 是一个包含两个项目的元组类型,这些项目分布在 xy 上,或者也许 x 的类型为 Anyy 的类型为 T?(后者是如果这发生在函数签名中,它的含义。)与其让(人类)读者猜测,我们禁止这样做,至少目前是这样。

  • 带括号的形式 (var: type) 用于注释:它在 python-ideas 上被提出作为上述歧义的补救措施,但被拒绝,因为这种语法会很麻烦,好处很小,可读性很差。
  • 允许链式赋值中的注释:这存在与元组解包类似的歧义和可读性问题,例如在
    x: int = y = 1
    z = w: int = 1
    

    中,yz 的类型应该是什么?此外,第二行也很难解析。

  • 允许在 with for 语句中使用注释:这被拒绝了,因为在 for 中,它会使实际的可迭代对象难以发现,而在 with 中,它会使 CPython 的 LL(1) 解析器感到困惑。
  • 在函数定义时评估局部注释:Guido 拒绝了这一点,因为注释的放置强烈暗示它与周围代码位于相同的范围。
  • 在函数范围内也存储变量注释:在本地使用注释的价值不足以抵消在 *每个* 函数调用时创建和填充字典的成本。
  • 初始化未赋值的带注释的变量:有人在 python-ideas 上提议将 xx: int 中初始化为 None 或另一个特殊的常量,如 Javascript 的 undefined。但是,向语言中添加另一个单例值需要在代码中的每个地方都进行检查。因此,Guido 对此只是简单地说“不”。
  • 向 typing 模块添加 InstanceVar这是多余的,因为实例变量比类变量更常见。更常见的用法应该成为默认值。
  • 仅允许在方法中使用实例变量注释:问题是许多 __init__ 方法除了初始化实例变量之外还做了很多事情,而且对于(人类)来说,查找所有实例变量注释会更加困难。有时 __init__ 被分解成更多辅助方法,因此跟踪它们会更加困难。将实例变量注释放在类中,可以更容易地找到它们,并帮助代码的首次阅读者。
  • 使用语法 x: class t = v 用于类变量:这需要一个更复杂的解析器,并且 class 关键字会使简单的语法高亮显示器感到困惑。无论如何,我们需要让 ClassVar 将类变量存储到 __annotations__ 中,因此选择了一个更简单的语法。
  • 完全忘记 ClassVar有人提出这一点,因为 mypy 似乎在没有区分类变量和实例变量的方法的情况下也能正常工作。但是类型检查器可以使用这些额外信息做一些有用的事情,例如标记通过实例对类变量的意外赋值(这将创建一个隐藏类变量的实例变量)。它还可以标记具有可变默认值的实例变量,这是一个众所周知的危险。
  • 使用 ClassAttr 代替 ClassVarClassVar 更好的主要原因如下:许多东西都是类属性,例如方法、描述符等。但只有特定的属性在概念上是类变量(或者可能是常量)。
  • 不评估注释,将它们视为字符串:这与函数注释的行为不一致,函数注释始终会被评估。虽然这可能在将来被重新考虑,但在 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/ilevkivskyi/cpython/tree/pep-526


来源:https://github.com/python/peps/blob/main/peps/pep-0526.rst

上次修改时间:2024-06-11 22:12:09 GMT