PEP 526 - 变量注释的语法

状态

该 PEP 已被 BDFL 临时接受。有关更多颜色,请参阅接受消息:https 😕/mail.python.org/pipermail/python-dev/2016-September/146282.html

审稿人通知

这个 PEP 是在一个单独的 repo 中起草的: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 以使用变量注释语法而不是类型注释将提高存根的可读性。

  • 在普通注释和类型注释一起使用的情况下,很难区分:

    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中对合规的态度一致。)

全局和局部变量注释

locals 和 globals 的类型可以注释如下:

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.

类和实例变量注解

类型注解也可以用来注解类体和方法中的类和实例变量。特别是,无值表示法允许注释应该在or中初始化的实例变量。建议的语法如下: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是由类型模块定义的特殊类,它向静态类型检查器指示不应在实例上设置此变量。

请注意ClassVar,无论嵌套级别如何,参数都不能包含任何类型变量:如果是类型变量ClassVar[T]ClassVar[List[Set[T]]]则两者都是无效的。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

原因是global并且nonlocal不拥有变量;因此,类型注释属于拥有变量的范围。

仅允许单个分配目标和单个右侧值。此外,不能注释fororwith语句中使用的变量;它们可以提前注释,类似于元组拆包:

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
    
    

标准库和文档的更改

  • 新的协变类型ClassVar[T_co]被添加到typing模块中。它只接受一个应该是有效类型的参数,并用于注释不应在类实例上设置的类变量。此限制由静态检查器确保,但不是在运行时。请参阅classvar部分以获取有关.ClassVarClassVar
  • get_type_hints模块中的函数typing将被扩展,以便在运行时从模块和类以及函数中检索类型注释。注释作为从变量或参数到其类型提示的字典映射返回,并评估了前向引用。对于类,它返回一个映射(可能是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 解释器接受而不会质疑它——但随后的类型注释预计它是 aMutableMapping并且会失败。)

在运行时获取注解的推荐方法是使用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-'

因为它不会关心除了“它评估而不引发”之外的类型注释,遇到它的类型检查器将标记它,除非使用or禁用。# 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
    
    

    这样做的问题是,这def对几代 Python 程序员(和工具!)来说意味着“定义一个函数”,并且使用它来定义变量并不会增加清晰度。(虽然这当然是主观的。)

  • 使用基于函数的语法:建议使用. 尽管这种语法缓解了类型注释的一些问题,例如 AST 中缺少注释,但它并没有解决其他问题,例如可读性,并且它引入了可能的运行时开销。var = cast(annotation[, value])

  • 允许元组解包的类型注释:这会导致歧义:不清楚此语句的含义:

    xy都属于类型,还是我们T期望T成为分布在xand上的两个项目的元组类型y,或者可能是x具有类型Anyy具有类型T?(如果这发生在函数签名中,后者就是这意味着。)与其让(人类)读者猜测,我们至少现在禁止这样做。

  • 带括号 的注释形式:它是在 python-ideas 上提出的,作为对上述歧义的补救措施,但由于这种语法毛茸茸,好处微乎其微,可读性差,因此被拒绝。(var: type)

  • 允许链式赋值中的注释:这具有类似于元组解包的歧义和可读性问题,例如:

    x: int = y = 1
    z = w: int = 1
    
    

    它是模棱两可的,y和的类型应该z是什么?第二行也很难解析。

  • Allow annotations in with and for statement:这被拒绝了,因为for它会让人很难发现实际的可迭代对象,并且with会混淆 CPython 的 LL(1) 解析器。

  • 在函数定义时评估本地注释:这已被 Guido 拒绝,因为注释的位置强烈表明它与周围代码在同一范围内。

  • 在函数范围内也存储变量注释:在本地提供注释的价值不足以显着抵消在_每个_函数调用上创建和填充字典的成本。

  • 初始化没有赋值注释的变量:在 python-ideas 上提出了初始化为x或额外的特殊常量,如 Javascript 的. 但是,需要在代码中的任何地方检查向语言中添加另一个单例值。因此,Guido 只是对此直接说“不”。x: int``None``undefined

  • 还要添加 InstanceVar 到类型模块:这是多余的,因为实例变量比类变量更常见。更常见的用法应该是默认值。

  • 仅在方法中允许实例变量注释:问题是许多__init__方法除了初始化实例变量之外还做了很多事情,并且(对于人类)很难找到所有实例变量注释。有时会__init__被考虑到更多的辅助方法中,因此追赶它们变得更加困难。将实例变量注释放在类中可以更容易地找到它们,并有助于第一次阅读代码。

  • 对类变量****使用语法:这将需要一个更复杂的解析器,并且关键字会混淆简单的语法高亮显示。无论如何,我们需要将类变量存储到,因此选择了更简单的语法。x: class t = v class``ClassVar``__annotations__

  • 完全忘记 ClassVar 这是因为 mypy 似乎相处得很好,没有办法区分类和实例变量。但是类型检查器可以用额外的信息做有用的事情,例如通过实例标记对类变量的意外分配(这将创建一个隐藏类变量的实例变量)。它还可以标记具有可变默认值的实例变量,这是众所周知的危险。

  • 使用 ClassAttr 代替 ClassVar:更好的主要原因ClassVar如下:很多东西是类属性,例如方法、描述符等。但只有特定属性在概念上是类变量(或者可能是常量)。

  • 不要评估注释,将它们视为字符串:这将与始终评估的函数注释的行为不一致。尽管将来可能会重新考虑,但在PEP 484中决定这必须是一个单独的 PEP。

  • 在类文档字符串中注释变量类型:许多项目已经使用了各种文档字符串约定,通常没有太多的一致性,而且通常还不符合PEP 484注释语法。这也需要一个特殊的复杂解析器。这反过来又会破坏 PEP 的目的——与第三方类型检查工具合作。

  • 实现 __annotations__ 为描述符:建议禁止设置__annotations__为非字典或非无的内容。Guido 拒绝了这个想法,认为这是不必要的。相反,如果尝试更新__annotations__它而不是映射,则会引发 TypeError。

  • 将裸注解视为全局或非局部注解:被拒绝的提案更倾向于在函数体中存在没有赋值的注解不应涉及_任何_评估。相比之下,PEP 暗示如果目标比单个名称更复杂,则应在其出现在函数体中的位置对其“左侧部分”进行评估,以强制执行它已定义。例如,在此示例中:

    def foo(self):
        slef.name: str
    
    

    应该评估名称slef,以便如果未定义它(如本例中可能的那样:-),错误将在运行时被捕获。_这更符合有_初始值时发生的情况,因此预计会导致更少的意外。(还要注意,如果目标是self.name(这次拼写正确:-),优化编译器没有义务评估self,只要它可以证明它肯定会被定义。)

向后兼容性

此 PEP 完全向后兼容。

执行

在https://github.com/ilevkivskyi/cpython/tree/pep-526的 GitHub 存储库中可以找到 Python 3.6 的实现

版权

该文件已被置于公共领域。

posted @ 2022-04-05 22:52  码上的生活  阅读(181)  评论(0编辑  收藏  举报