Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 484 – 类型提示

作者:
Guido van Rossum <guido at python.org>, Jukka Lehtosalo <jukka.lehtosalo at iki.fi>, Łukasz Langa <lukasz at python.org>
BDFL 委托
Mark Shannon
讨论至:
Python-Dev 列表
状态:
最终版
类型:
标准跟踪
主题:
类型标注
创建日期:
2014年9月29日
Python 版本:
3.5
发布历史:
2015年1月16日,2015年3月20日,2015年4月17日,2015年5月20日,2015年5月22日
决议:
Python-Dev 消息

目录

摘要

PEP 3107 引入了函数注解的语法,但其语义被故意留白。现在已经有足够的第三方用法用于静态类型分析,社区将受益于标准库中的标准词汇和基础工具。

本 PEP 引入了一个临时模块来提供这些标准定义和工具,以及在没有注解的情况下的一些约定。

请注意,本 PEP 仍明确不阻止注解的其他用途,也不要求(或禁止)对注解进行任何特定的处理,即使它们符合本规范。它只是实现了更好的协调,就像 PEP 333 对 Web 框架所做的那样。

例如,这是一个简单的函数,其参数和返回类型在注解中声明:

def greeting(name: str) -> str:
    return 'Hello ' + name

虽然这些注解在运行时通过通常的 __annotations__ 属性可用,但 运行时不进行类型检查。相反,该提案假定存在一个单独的离线类型检查器,用户可以自愿在其源代码上运行。本质上,这样的类型检查器充当一个非常强大的 linter。(虽然个人用户当然可以在运行时使用类似的检查器来实现契约式设计强制或 JIT 优化,但这些工具尚未成熟。)

该提案深受 mypy 的启发。例如,“整数序列”类型可以写成 Sequence[int]。方括号意味着无需向语言添加新语法。此处的示例使用了一个自定义类型 Sequence,它从纯 Python 模块 typing 导入。Sequence[int] 表示法通过在元类中实现 __getitem__() 在运行时工作(但其意义主要在于离线类型检查器)。

类型系统支持联合、泛型类型和名为 Any 的特殊类型,该类型与所有类型一致(即可以相互赋值)。后一个特性借鉴了渐进式类型(gradual typing)的思想。渐进式类型和完整的类型系统在 PEP 483 中有解释。

我们借鉴或可以比较和对比的其他方法在 PEP 482 中有描述。

基本原理和目标

PEP 3107 添加了对函数定义部分任意注解的支持。虽然当时没有为注解赋予任何含义,但一直存在一个 隐式目标是将其用于类型提示,该 PEP 中列出的第一个可能用例就是它。

本 PEP 旨在提供一种标准的类型注解语法,从而使 Python 代码更容易进行静态分析和重构,实现潜在的运行时类型检查,以及(或许在某些上下文中)利用类型信息生成代码。

在这些目标中,静态分析最为重要。这包括对离线类型检查器(如 mypy)的支持,以及提供可供 IDE 用于代码补全和重构的标准表示法。

非目标

虽然提议的 typing 模块将包含一些用于运行时类型检查的构建块——特别是 get_type_hints() 函数——但必须开发第三方包才能实现特定的运行时类型检查功能,例如使用装饰器或元类。将类型提示用于性能优化则留给读者自行探索。

还应强调的是,Python 将继续是一种动态类型语言,作者绝不希望强制要求使用类型提示,即使是约定俗成也不行。

注解的含义

任何没有注解的函数,任何类型检查器都应将其视为具有最通用类型,或将其忽略。带有 @no_type_check 装饰器的函数应被视为没有注解。

建议但不要求受检查的函数对其所有参数和返回类型都进行注解。对于受检查的函数,参数和返回类型的默认注解是 Any。实例方法和类方法的第一个参数是例外。如果它未被注解,则假定实例方法具有包含类的类型,而类方法具有与包含类对象对应的类型对象类型。例如,在类 A 中,实例方法的第一个参数具有隐式类型 A。在类方法中,第一个参数的精确类型无法使用可用的类型表示法表示。

(请注意,__init__ 的返回类型应该用 -> None 进行注解。原因很微妙。如果 __init__ 假定返回注解为 -> None,那是否意味着一个无参数、无注解的 __init__ 方法仍应进行类型检查?与其留下这种歧义或引入一个例外中的例外,我们不如简单地说 __init__ 应该有一个返回注解;因此默认行为与其他方法相同。)

类型检查器应检查已检查函数的主体与给定注解的一致性。注解还可用于检查出现在其他已检查函数中的调用的正确性。

类型检查器应该尝试推断尽可能多的信息。最低要求是处理内置装饰器 @property@staticmethod@classmethod

类型定义语法

该语法利用 PEP 3107 风格的注解,并进行了下述章节中描述的一些扩展。其基本形式是,通过用类填充函数注解槽来使用类型提示:

def greeting(name: str) -> str:
    return 'Hello ' + name

这表明 name 参数的预期类型是 str。类似地,预期的返回类型也是 str

其类型是特定参数类型的子类型的表达式也适用于该参数。

可接受的类型提示

类型提示可以是内置类(包括标准库或第三方扩展模块中定义的类)、抽象基类、types 模块中可用的类型以及用户定义的类(包括标准库或第三方模块中定义的类)。

虽然注解通常是类型提示的最佳格式,但在某些情况下,用特殊注释或在单独分发的存根文件中表示它们更合适。(请参阅下面的示例。)

在函数定义时,注解必须是有效表达式,并且在求值时不会引发异常(但请参阅下面的前向引用)。

注解应保持简单,否则静态分析工具可能无法解释其值。例如,动态计算的类型不太可能被理解。(这是一个有意含糊的要求,具体包含和排除项可能会根据讨论的需要添加到本 PEP 的未来版本中。)

除上述之外,还可以使用下面定义的以下特殊构造:NoneAnyUnionTupleCallable、所有 ABC 和从 typing 导出的具体类的替代品(例如 SequenceDict)、类型变量和类型别名。

所有用于支持以下章节中描述的特性(例如 AnyUnion)的新引入名称都可以在 typing 模块中找到。

使用 None

在类型提示中使用时,表达式 None 被认为等同于 type(None)

类型别名

类型别名通过简单的变量赋值来定义:

Url = str

def retry(url: Url, retry_count: int) -> None: ...

请注意,我们建议将别名首字母大写,因为它们代表用户定义的类型,通常(像用户定义的类一样)以这种方式拼写。

类型别名可以像注解中的类型提示一样复杂——任何可接受的类型提示都可以在类型别名中使用:

from typing import TypeVar, Iterable, Tuple

T = TypeVar('T', int, float, complex)
Vector = Iterable[Tuple[T, T]]

def inproduct(v: Vector[T]) -> T:
    return sum(x*y for x, y in v)
def dilate(v: Vector[T], scale: T) -> Vector[T]:
    return ((x * scale, y * scale) for x, y in v)
vec = []  # type: Vector[float]

这等同于:

from typing import TypeVar, Iterable, Tuple

T = TypeVar('T', int, float, complex)

def inproduct(v: Iterable[Tuple[T, T]]) -> T:
    return sum(x*y for x, y in v)
def dilate(v: Iterable[Tuple[T, T]], scale: T) -> Iterable[Tuple[T, T]]:
    return ((x * scale, y * scale) for x, y in v)
vec = []  # type: Iterable[Tuple[float, float]]

可调用对象

期望特定签名的回调函数的框架可以使用 Callable[[Arg1Type, Arg2Type], ReturnType] 进行类型提示。例如:

from typing import Callable

def feeder(get_next_item: Callable[[], str]) -> None:
    # Body

def async_query(on_success: Callable[[int], None],
                on_error: Callable[[int, Exception], None]) -> None:
    # Body

通过用字面省略号(三个点)代替参数列表,可以声明可调用对象的返回类型而不指定调用签名:

def partial(func: Callable[..., str], *args) -> Callable[..., str]:
    # Body

请注意,省略号周围没有方括号。在这种情况下,回调的参数是完全不受约束的(并且关键字参数是可接受的)。

由于使用带有关键字参数的回调不被认为是一种常见的用例,因此目前不支持使用 Callable 指定关键字参数。同样,也不支持指定带有特定类型可变数量参数的回调签名。

由于 typing.Callable 兼作 collections.abc.Callable 的替代品,isinstance(x, typing.Callable) 通过委托给 isinstance(x, collections.abc.Callable) 来实现。但是,不支持 isinstance(x, typing.Callable[...])

泛型

由于无法以通用的方式静态推断容器中对象的类型信息,因此抽象基类已扩展为支持下标以表示容器元素的预期类型。例如:

from typing import Mapping, Set

def notify_by_email(employees: Set[Employee], overrides: Mapping[str, str]) -> None: ...

泛型可以通过使用 typing 中名为 TypeVar 的新工厂进行参数化。例如:

from typing import Sequence, TypeVar

T = TypeVar('T')      # Declare type variable

def first(l: Sequence[T]) -> T:   # Generic function
    return l[0]

在这种情况下,契约是返回的值与集合持有的元素一致。

TypeVar() 表达式必须始终直接赋值给变量(它不应作为较大表达式的一部分使用)。TypeVar() 的参数必须是一个字符串,该字符串与它被赋值的变量名相同。类型变量不得重新定义。

TypeVar 支持将参数化类型约束为一组固定的可能类型(注意:这些类型不能由类型变量参数化)。例如,我们可以定义一个只包含 strbytes 的类型变量。默认情况下,类型变量涵盖所有可能的类型。约束类型变量的示例:

from typing import TypeVar, Text

AnyStr = TypeVar('AnyStr', Text, bytes)

def concat(x: AnyStr, y: AnyStr) -> AnyStr:
    return x + y

函数 concat 可以用两个 str 参数或两个 bytes 参数调用,但不能混合使用 strbytes 参数。

如果有约束,则至少应有两个;不允许指定单个约束。

在类型变量的上下文中,类型变量约束的子类型应被视为其各自明确列出的基类型。考虑这个例子:

class MyStr(str): ...

x = concat(MyStr('apple'), MyStr('pie'))

调用是有效的,但类型变量 AnyStr 将设置为 str 而不是 MyStr。实际上,赋值给 x 的返回值的推断类型也将是 str

此外,Any 是每个类型变量的有效值。考虑以下情况:

def count_truthy(elements: List[Any]) -> int:
    return sum(1 for elem in elements if elem)

这等同于省略泛型表示法,只写 elements: List

用户定义的泛型类型

你可以包含一个 Generic 基类来将用户定义的类定义为泛型。例如:

from typing import TypeVar, Generic
from logging import Logger

T = TypeVar('T')

class LoggedVar(Generic[T]):
    def __init__(self, value: T, name: str, logger: Logger) -> None:
        self.name = name
        self.logger = logger
        self.value = value

    def set(self, new: T) -> None:
        self.log('Set ' + repr(self.value))
        self.value = new

    def get(self) -> T:
        self.log('Get ' + repr(self.value))
        return self.value

    def log(self, message: str) -> None:
        self.logger.info('{}: {}'.format(self.name, message))

Generic[T] 作为基类定义了类 LoggedVar 接受单个类型参数 T。这也使得 T 在类体中作为类型有效。

Generic 基类使用一个定义 __getitem__ 的元类,以便 LoggedVar[t] 作为类型有效:

from typing import Iterable

def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
    for var in vars:
        var.set(0)

一个泛型类型可以有任意数量的类型变量,并且类型变量可以被约束。这是有效的:

from typing import TypeVar, Generic
...

T = TypeVar('T')
S = TypeVar('S')

class Pair(Generic[T, S]):
    ...

传递给 Generic 的每个类型变量参数必须是不同的。因此,这是无效的:

from typing import TypeVar, Generic
...

T = TypeVar('T')

class Pair(Generic[T, T]):   # INVALID
    ...

在简单情况下,当你继承其他泛型类并为其参数指定类型变量时,Generic[T] 基类是冗余的:

from typing import TypeVar, Iterator

T = TypeVar('T')

class MyIter(Iterator[T]):
    ...

该类定义等同于:

class MyIter(Iterator[T], Generic[T]):
    ...

你可以使用多重继承与 Generic

from typing import TypeVar, Generic, Sized, Iterable, Container, Tuple

T = TypeVar('T')

class LinkedList(Sized, Generic[T]):
    ...

K = TypeVar('K')
V = TypeVar('V')

class MyMapping(Iterable[Tuple[K, V]],
                Container[Tuple[K, V]],
                Generic[K, V]):
    ...

继承泛型类而不指定类型参数,将假定每个位置都为 Any。在以下示例中,MyIterable 不是泛型,但隐式继承自 Iterable[Any]

from typing import Iterable

class MyIterable(Iterable):  # Same as Iterable[Any]
    ...

不支持泛型元类。

类型变量的作用域规则

类型变量遵循正常的名称解析规则。但是,在静态类型检查上下文中存在一些特殊情况:

  • 在泛型函数中使用的类型变量可以在同一代码块中推断出表示不同的类型。例如:
    from typing import TypeVar, Generic
    
    T = TypeVar('T')
    
    def fun_1(x: T) -> T: ...  # T here
    def fun_2(x: T) -> T: ...  # and here could be different
    
    fun_1(1)                   # This is OK, T is inferred to be int
    fun_2('a')                 # This is also OK, now T is str
    
  • 在泛型类的方法中使用的类型变量,如果与参数化该类的变量之一重合,则始终绑定到该变量。示例:
    from typing import TypeVar, Generic
    
    T = TypeVar('T')
    
    class MyClass(Generic[T]):
        def meth_1(self, x: T) -> T: ...  # T here
        def meth_2(self, x: T) -> T: ...  # and here are always the same
    
    a = MyClass()  # type: MyClass[int]
    a.meth_1(1)    # OK
    a.meth_2('a')  # This is an error!
    
  • 在方法中使用的类型变量,如果与参数化该类的任何变量不匹配,则使该方法成为该变量的泛型函数:
    T = TypeVar('T')
    S = TypeVar('S')
    class Foo(Generic[T]):
        def method(self, x: T, y: S) -> S:
            ...
    
    x = Foo()               # type: Foo[int]
    y = x.method(0, "abc")  # inferred type of y is str
    
  • 未绑定的类型变量不应出现在泛型函数的主体中,或除方法定义之外的类主体中:
    T = TypeVar('T')
    S = TypeVar('S')
    
    def a_fun(x: T) -> None:
        # this is OK
        y = []  # type: List[T]
        # but below is an error!
        y = []  # type: List[S]
    
    class Bar(Generic[T]):
        # this is also an error
        an_attr = []  # type: List[S]
    
        def do_something(x: S) -> S:  # this is OK though
            ...
    
  • 出现在泛型函数内部的泛型类定义不应使用参数化泛型函数的类型变量:
    from typing import List
    
    def a_fun(x: T) -> None:
    
        # This is OK
        a_list = []  # type: List[T]
        ...
    
        # This is however illegal
        class MyGeneric(Generic[T]):
            ...
    
  • 嵌套在另一个泛型类中的泛型类不能使用相同的类型变量。外部类的类型变量作用域不覆盖内部类:
    T = TypeVar('T')
    S = TypeVar('S')
    
    class Outer(Generic[T]):
        class Bad(Iterable[T]):       # Error
            ...
        class AlsoBad:
            x = None  # type: List[T] # Also an error
    
        class Inner(Iterable[S]):     # OK
            ...
        attr = None  # type: Inner[T] # Also OK
    

泛型类的实例化和类型擦除

用户定义的泛型类可以实例化。假设我们编写一个继承自 Generic[T]Node 类:

from typing import TypeVar, Generic

T = TypeVar('T')

class Node(Generic[T]):
    ...

要创建 Node 实例,你只需像常规类一样调用 Node()。在运行时,实例的类型(类)将是 Node。但对于类型检查器而言,它是什么类型呢?答案取决于调用中可用信息的多少。如果构造函数(__init____new__)在其签名中使用了 T,并且传递了相应的参数值,则替换相应参数的类型。否则,假定为 Any。例如:

from typing import TypeVar, Generic

T = TypeVar('T')

class Node(Generic[T]):
    x = None  # type: T # Instance attribute (see below)
    def __init__(self, label: T = None) -> None:
        ...

x = Node('')  # Inferred type is Node[str]
y = Node(0)   # Inferred type is Node[int]
z = Node()    # Inferred type is Node[Any]

如果推断类型使用 [Any] 但预期类型更具体,你可以使用类型注释(参见下文)来强制变量的类型,例如:

# (continued from previous example)
a = Node()  # type: Node[int]
b = Node()  # type: Node[str]

或者,你可以实例化一个特定的具体类型,例如:

# (continued from previous example)
p = Node[int]()
q = Node[str]()
r = Node[int]('')  # Error
s = Node[str](0)   # Error

请注意,pq 的运行时类型(类)仍然只是 Node——Node[int]Node[str] 是可区分的类对象,但通过实例化它们创建的对象的运行时类不记录这种区别。这种行为被称为“类型擦除”;这在泛型语言(例如 Java、TypeScript)中很常见。

使用泛型类(参数化或不参数化)访问属性将导致类型检查失败。在类定义体之外,类属性不能被赋值,并且只能通过访问没有同名实例属性的类实例来查找:

# (continued from previous example)
Node[int].x = 1  # Error
Node[int].x      # Error
Node.x = 1       # Error
Node.x           # Error
type(p).x        # Error
p.x              # Ok (evaluates to None)
Node[int]().x    # Ok (evaluates to None)
p.x = 1          # Ok, but assigning to instance attribute

MappingSequence 这样的抽象集合的泛型版本以及内置类(ListDictSetFrozenSet)的泛型版本无法实例化。然而,用户定义的具体子类和具体集合的泛型版本可以实例化:

data = DefaultDict[int, bytes]()

请注意,不应混淆静态类型和运行时类。在这种情况下,类型仍然被擦除,上述表达式只是一个简写:

data = collections.defaultdict()  # type: DefaultDict[int, bytes]

不建议直接在表达式中使用带下标的类(例如 Node[int])——首选使用类型别名(例如 IntNode = Node[int])。(首先,创建带下标的类,例如 Node[int],具有运行时成本。其次,使用类型别名更具可读性。)

作为基类的任意泛型类型

Generic[T] 仅作为基类有效——它不是一个适当的类型。然而,用户定义的泛型类型(如上述示例中的 LinkedList[T])以及内置泛型类型和 ABC(如 List[T]Iterable[T])既可以作为类型也可以作为基类。例如,我们可以定义 Dict 的一个子类,它专门化类型参数:

from typing import Dict, List, Optional

class Node:
    ...

class SymbolTable(Dict[str, List[Node]]):
    def push(self, name: str, node: Node) -> None:
        self.setdefault(name, []).append(node)

    def pop(self, name: str) -> Node:
        return self[name].pop()

    def lookup(self, name: str) -> Optional[Node]:
        nodes = self.get(name)
        if nodes:
            return nodes[-1]
        return None

SymbolTabledict 的子类,也是 Dict[str, List[Node]] 的子类型。

如果泛型基类具有类型变量作为类型参数,则这将使定义的类成为泛型。例如,我们可以定义一个可迭代且是容器的泛型 LinkedList 类:

from typing import TypeVar, Iterable, Container

T = TypeVar('T')

class LinkedList(Iterable[T], Container[T]):
    ...

现在 LinkedList[int] 是一个有效类型。请注意,我们可以在基类列表中多次使用 T,只要我们不在 Generic[...] 中多次使用相同的类型变量 T

另外考虑以下示例:

from typing import TypeVar, Mapping

T = TypeVar('T')

class MyDict(Mapping[str, T]):
    ...

在这种情况下,MyDict 只有一个参数,T。

抽象泛型类型

Generic 所使用的元类是 abc.ABCMeta 的子类。泛型类可以通过包含抽象方法或属性成为 ABC,泛型类也可以将 ABC 作为基类而不会产生元类冲突。

具有上限的类型变量

类型变量可以通过 bound=<type> 指定上限(注意:<type> 本身不能由类型变量参数化)。这意味着实际替换(显式或隐式)类型变量的类型必须是边界类型的子类型。例如:

from typing import TypeVar, Sized

ST = TypeVar('ST', bound=Sized)

def longer(x: ST, y: ST) -> ST:
    if len(x) > len(y):
        return x
    else:
        return y

longer([1], [1, 2])  # ok, return type List[int]
longer({1}, {1, 2})  # ok, return type Set[int]
longer([1], {1, 2})  # ok, return type Collection[int]

上限不能与类型约束结合使用(如 AnyStr 示例所示);类型约束导致推断类型_恰好_是约束类型之一,而上限只要求实际类型是边界类型的子类型。

协变与逆变

考虑一个类 Employee 及其子类 Manager。现在假设我们有一个函数,其参数被注解为 List[Employee]。我们是否应该允许用类型为 List[Manager] 的变量作为参数来调用此函数?许多人会不假思索地回答“是,当然”。但是,除非我们对函数有更多了解,否则类型检查器应该拒绝这样的调用:该函数可能会向列表中添加一个 Employee 实例,这将违反调用者中变量的类型。

事实证明,这样的参数表现出 逆变性,而直观的答案(在函数不修改其参数的情况下是正确的!)要求参数表现出 协变性。关于这些概念的更长篇介绍可以在 维基百科PEP 483 中找到;这里我们只展示如何控制类型检查器的行为。

默认情况下,泛型类型在其所有类型变量中都被认为是 不变的,这意味着用 List[Employee] 等类型注解的变量的值必须与类型注解完全匹配——不允许使用类型参数(在此示例中为 Employee)的子类或超类。

为了方便声明协变或逆变类型检查可接受的容器类型,类型变量接受关键字参数 covariant=Truecontravariant=True。最多只能传递其中一个。使用此类变量定义的泛型类型在其相应的变量中被认为是协变或逆变的。按照惯例,建议对使用 covariant=True 定义的类型变量使用以 _co 结尾的名称,对使用 contravariant=True 定义的类型变量使用以 _contra 结尾的名称。

一个典型的例子是定义一个不可变(或只读)的容器类:

from typing import TypeVar, Generic, Iterable, Iterator

T_co = TypeVar('T_co', covariant=True)

class ImmutableList(Generic[T_co]):
    def __init__(self, items: Iterable[T_co]) -> None: ...
    def __iter__(self) -> Iterator[T_co]: ...
    ...

class Employee: ...

class Manager(Employee): ...

def dump_employees(emps: ImmutableList[Employee]) -> None:
    for emp in emps:
        ...

mgrs = ImmutableList([Manager()])  # type: ImmutableList[Manager]
dump_employees(mgrs)  # OK

typing 中的只读集合类都在其类型变量中声明为协变(例如 MappingSequence)。可变集合类(例如 MutableMappingMutableSequence)声明为不变。协变类型的一个例子是 Generator 类型,它在其 send() 参数类型中是逆变的(参见下文)。

注意:协变性或逆变性 不是 类型变量的属性,而是使用该变量定义的泛型类的属性。变体仅适用于泛型类型;泛型函数不具有此属性。后者应仅使用不带 covariantcontravariant 关键字参数的类型变量进行定义。例如,以下示例是正确的:

from typing import TypeVar

class Employee: ...

class Manager(Employee): ...

E = TypeVar('E', bound=Employee)

def dump_employee(e: E) -> None: ...

dump_employee(Manager())  # OK

而以下是被禁止的:

B_co = TypeVar('B_co', covariant=True)

def bad_func(x: B_co) -> B_co:  # Flagged as error by a type checker
    ...

数字塔

PEP 3141 定义了 Python 的数字塔,而标准库模块 numbers 实现了相应的 ABC(NumberComplexRealRationalIntegral)。这些 ABC 存在一些问题,但内置的具体数字类 complexfloatint 无处不在(尤其是后两者 :-)。

本 PEP 提出了一种简单直接的捷径,几乎同样有效,而无需用户编写 import numbers 然后使用 numbers.Float 等:当参数被注解为 float 类型时,int 类型的参数是可接受的;类似地,当参数被注解为 complex 类型时,floatint 类型的参数是可接受的。这不处理实现相应 ABC 的类或 fractions.Fraction 类,但我们认为这些用例极其罕见。

前向引用

当类型提示包含尚未定义的名称时,该定义可以表示为字符串字面量,以便稍后解析。

这种情况通常发生在容器类的定义中,其中被定义的类出现在某些方法的签名中。例如,以下代码(一个简单的二叉树实现的开始)无法工作:

class Tree:
    def __init__(self, left: Tree, right: Tree):
        self.left = left
        self.right = right

为了解决这个问题,我们写:

class Tree:
    def __init__(self, left: 'Tree', right: 'Tree'):
        self.left = left
        self.right = right

字符串字面量应包含有效的 Python 表达式(即,compile(lit, '', 'eval') 应该是一个有效的代码对象),并且在模块完全加载后,它应该能够无错误地求值。求值时使用的局部和全局命名空间应与同一函数的默认参数求值时使用的命名空间相同。

此外,该表达式应该能够解析为有效的类型提示,即它受到上述 可接受的类型提示 部分规则的约束。

允许使用字符串字面量作为类型提示的 一部分,例如:

class Tree:
    ...
    def leaves(self) -> List['Tree']:
        ...

前向引用的一个常见用途是当在签名中需要 Django 模型时。通常,每个模型都在一个单独的文件中,并且其方法接受的参数类型涉及其他模型。由于 Python 中循环导入的工作方式,通常无法直接导入所有需要的模型:

# File models/a.py
from models.b import B
class A(Model):
    def foo(self, b: B): ...

# File models/b.py
from models.a import A
class B(Model):
    def bar(self, a: A): ...

# File main.py
from models.a import A
from models.b import B

假设首先导入 main,这将在 models/b.py 中导入 models/a.py 之前在 models/b.py 中导致 ImportError,因为 models/a.py 尚未定义类 A。解决方案是切换到仅模块导入并通过其 _模块_._类_ 名称引用模型:

# File models/a.py
from models import b
class A(Model):
    def foo(self, b: 'b.B'): ...

# File models/b.py
from models import a
class B(Model):
    def bar(self, a: 'a.A'): ...

# File main.py
from models.a import A
from models.b import B

联合类型

由于单个参数接受少量有限的预期类型是常见的,因此有一个名为 Union 的新特殊工厂。例如:

from typing import Union

def handle_employees(e: Union[Employee, Sequence[Employee]]) -> None:
    if isinstance(e, Employee):
        e = [e]
    ...

Union[T1, T2, ...] 构成的类型是所有类型 T1T2 等的超类型,因此属于这些类型之一的值对于由 Union[T1, T2, ...] 注解的参数是可接受的。

联合类型的一种常见情况是 可选 类型。默认情况下,None 对于任何类型都是无效值,除非在函数定义中提供了默认值 None。示例:

def handle_employee(e: Union[Employee, None]) -> None: ...

作为 Union[T1, None] 的简写,你可以写 Optional[T1];例如,上面等同于:

from typing import Optional

def handle_employee(e: Optional[Employee]) -> None: ...

本 PEP 的早期版本允许类型检查器在默认值为 None 时假定为可选类型,如以下代码所示:

def handle_employee(e: Employee = None): ...

这将被视为等同于:

def handle_employee(e: Optional[Employee] = None) -> None: ...

这不再是推荐的行为。类型检查器应朝着要求明确可选类型的方向发展。

联合中单例类型的支持

单例实例常用于标记某些特殊情况,尤其是在 None 也是变量有效值的情况下。示例:

_empty = object()

def func(x=_empty):
    if x is _empty:  # default argument value
        return 0
    elif x is None:  # argument was provided and it's None
        return 1
    else:
        return x * 2

为了在这种情况下实现精确的类型提示,用户应将 Union 类型与标准库提供的 enum.Enum 类结合使用,以便静态捕获类型错误:

from typing import Union
from enum import Enum

class Empty(Enum):
    token = 0
_empty = Empty.token

def func(x: Union[int, None, Empty] = _empty) -> int:

    boom = x * 42  # This fails type check

    if x is _empty:
        return 0
    elif x is None:
        return 1
    else:  # At this point typechecker knows that x can only have type int
        return x * 2

由于 Enum 的子类不能再被继承,因此在上述示例的所有分支中,变量 x 的类型都可以静态推断。如果需要多个单例对象,也适用相同的方法:可以使用具有多个值的枚举:

class Reason(Enum):
    timeout = 1
    error = 2

def process(response: Union[str, Reason] = '') -> str:
    if response is Reason.timeout:
        return 'TIMEOUT'
    elif response is Reason.error:
        return 'ERROR'
    else:
        # response can be only str, all other possible values exhausted
        return 'PROCESSED: ' + response

Any 类型

一种特殊类型是 Any。每种类型都与 Any 兼容。它可以被认为是一种拥有所有值和所有方法的类型。请注意,Any 和内置类型 object 完全不同。

当一个值的类型是 object 时,类型检查器会拒绝对其进行几乎所有操作,将其赋值给更专用类型的变量(或将其用作返回值)是一种类型错误。另一方面,当一个值的类型是 Any 时,类型检查器会允许对其进行所有操作,并且 Any 类型的值可以赋值给更受约束的类型的变量(或用作返回值)。

没有注解的函数参数被假定为用 Any 注解。如果泛型类型在没有指定类型参数的情况下使用,则它们被假定为 Any

from typing import Mapping

def use_map(m: Mapping) -> None:  # Same as Mapping[Any, Any]
    ...

此规则也适用于 Tuple,在注解上下文中它等同于 Tuple[Any, ...],进而等同于 tuple。同样,注解中裸露的 Callable 等同于 Callable[..., Any],进而等同于 collections.abc.Callable

from typing import Tuple, List, Callable

def check_args(args: Tuple) -> bool:
    ...

check_args(())           # OK
check_args((42, 'abc'))  # Also OK
check_args(3.14)         # Flagged as error by a type checker

# A list of arbitrary callables is accepted by this function
def apply_callbacks(cbs: List[Callable]) -> None:
    ...

NoReturn 类型

typing 模块提供了一个特殊类型 NoReturn 来注解从不正常返回的函数。例如,一个无条件引发异常的函数:

from typing import NoReturn

def stop() -> NoReturn:
    raise RuntimeError('no way')

NoReturn 注解用于像 sys.exit 这样的函数。静态类型检查器将确保注解为返回 NoReturn 的函数确实永不返回,无论是隐式还是显式:

import sys
from typing import NoReturn

  def f(x: int) -> NoReturn:  # Error, f(0) implicitly returns None
      if x != 0:
          sys.exit(1)

检查器还将识别出此类函数调用之后的代码是不可达的,并会相应地处理:

# continue from first example
def g(x: int) -> int:
    if x > 0:
        return x
    stop()
    return 'whatever works'  # Error might be not reported by some checkers
                             # that ignore errors in unreachable blocks

NoReturn 类型仅在函数返回注解中有效,出现在其他位置被认为是错误的:

from typing import List, NoReturn

# All of the following are errors
def bad1(x: NoReturn) -> int:
    ...
bad2 = None  # type: NoReturn
def bad3() -> List[NoReturn]:
    ...

类对象的类型

有时你想讨论类对象,特别是那些继承自给定类的类对象。这可以表示为 Type[C],其中 C 是一个类。需要澄清的是:当 C(用作注解时)指代 C 类的实例时,Type[C] 指代 C子类。(这与 objecttype 之间的区别类似。)

例如,假设我们有以下类:

class User: ...  # Abstract base for User classes
class BasicUser(User): ...
class ProUser(User): ...
class TeamUser(User): ...

假设我们有一个函数,如果你传递一个类对象给它,它会创建这些类中的一个实例:

def new_user(user_class):
    user = user_class()
    # (Here we could write the user object to a database)
    return user

如果没有 Type[],我们能做的最好的注解 new_user() 的方式是:

def new_user(user_class: type) -> User:
    ...

然而,使用 Type[] 和带有上限的类型变量,我们可以做得更好:

U = TypeVar('U', bound=User)
def new_user(user_class: Type[U]) -> U:
    ...

现在,当我们用 User 的特定子类调用 new_user() 时,类型检查器将推断出结果的正确类型:

joe = new_user(BasicUser)  # Inferred type is BasicUser

对应于 Type[C] 的值必须是一个实际的类对象,它是 C 的子类型,而不是特殊形式。换句话说,在上面的例子中,调用例如 new_user(Union[BasicUser, ProUser]) 会被类型检查器拒绝(此外在运行时也会失败,因为你无法实例化联合)。

请注意,将类的联合用作 Type[] 的参数是合法的,例如:

def new_non_team_user(user_class: Type[Union[BasicUser, ProUser]]):
    user = new_user(user_class)
    ...

然而,运行时传入的实际参数仍然必须是一个具体的类对象,例如在上述示例中:

new_non_team_user(ProUser)  # OK
new_non_team_user(TeamUser)  # Disallowed by type checker

还支持 Type[Any](有关其含义,请参阅下文)。

当注解类方法(参见相关章节)的第一个参数时,允许使用 Type[T],其中 T 是一个类型变量。

任何其他特殊构造,如 TupleCallable,不允许作为 Type 的参数。

这个特性存在一些担忧:例如,当 new_user() 调用 user_class() 时,这意味着 User 的所有子类都必须在其构造函数签名中支持此功能。然而,这并非 Type[] 独有:类方法也有类似的担忧。类型检查器应该标记此类假设的违规行为,但默认情况下,匹配指定基类(上例中的 User)中构造函数签名的构造函数调用应被允许。包含复杂或可扩展类层次结构的程序也可以通过使用工厂类方法来处理此问题。本 PEP 的未来修订版可能会引入更好的方法来处理这些担忧。

Type 参数化时,它需要恰好一个参数。不带方括号的纯 Type 等同于 Type[Any],而这又等同于 type(Python 元类层次结构的根)。这种等价性也促使了名称 Type,而不是在讨论此功能时提出的 ClassSubType 等替代名称;这类似于例如 Listlist 之间的关系。

关于 Type[Any](或 Typetype)的行为,访问此类型变量的属性仅提供由 type 定义的属性和方法(例如,__repr__()__mro__)。这样的变量可以接受任意参数进行调用,并且返回类型是 Any

Type 在其参数上是协变的,因为 Type[Derived]Type[Base] 的子类型。

def new_pro_user(pro_user_class: Type[ProUser]):
    user = new_user(pro_user_class)  # OK
    ...

实例方法和类方法的注解

在大多数情况下,类方法和实例方法的第一个参数不需要注解,并且假定实例方法具有包含类的类型,而类方法具有与包含类对象对应的类型对象类型。此外,实例方法中的第一个参数可以用类型变量注解。在这种情况下,返回类型可以使用相同的类型变量,从而使该方法成为一个泛型函数。例如:

T = TypeVar('T', bound='Copyable')
class Copyable:
    def copy(self: T) -> T:
        # return a copy of self

class C(Copyable): ...
c = C()
c2 = c.copy()  # type here should be C

同样适用于类方法,其第一个参数的注解中使用了 Type[]

T = TypeVar('T', bound='C')
class C:
    @classmethod
    def factory(cls: Type[T]) -> T:
        # make a new instance of cls

class D(C): ...
d = D.factory()  # type here should be D

请注意,某些类型检查器可能会对此用法施加限制,例如要求所使用的类型变量具有适当的上限(请参阅示例)。

版本和平台检查

类型检查器应该能够理解简单的版本和平台检查,例如:

import sys

if sys.version_info[0] >= 3:
    # Python 3 specific definitions
else:
    # Python 2 specific definitions

if sys.platform == 'win32':
    # Windows specific definitions
else:
    # Posix specific definitions

不要指望检查器能理解像 "".join(reversed(sys.platform)) == "xunil" 这样的混淆。

运行时还是类型检查?

有时有些代码必须被类型检查器(或其他静态分析工具)看到,但不应被执行。对于这种情况,typing 模块定义了一个常量 TYPE_CHECKING,它在类型检查(或其他静态分析)期间被认为是 True,但在运行时被认为是 False。例如:

import typing

if typing.TYPE_CHECKING:
    import expensive_mod

def a_func(arg: 'expensive_mod.SomeClass') -> None:
    a_var = arg  # type: expensive_mod.SomeClass
    ...

(请注意,类型注解必须用引号括起来,使其成为一个“前向引用”,以将 expensive_mod 引用隐藏起来,不让解释器运行时看到。在 # type 注释中不需要引号。)

这种方法也可用于处理导入循环。

任意参数列表和默认参数值

任意参数列表也可以进行类型注解,因此以下定义:

def foo(*args: str, **kwds: int): ...

是可接受的,这意味着,例如,以下所有函数调用都具有有效的参数类型:

foo('a', 'b', 'c')
foo(x=1, y=2)
foo('', z=0)

在函数 foo 的函数体中,变量 args 的类型被推断为 Tuple[str, ...],变量 kwds 的类型被推断为 Dict[str, int]

在存根文件中,声明参数具有默认值而不指定实际默认值可能很有用。例如:

def foo(x: AnyStr, y: AnyStr = ...) -> AnyStr: ...

默认值应该是什么样子?""b""None 都不符合类型约束。

在这种情况下,默认值可以指定为字面省略号,即上面的例子就是你所写的内容。

仅位置参数

有些函数被设计为只接受位置参数,并期望它们的调用者从不使用参数名通过关键字提供该参数。所有以 __ 开头的参数都被假定为仅位置参数,除非它们的名称也以 __ 结尾:

def quux(__x: int, __y__: int = 0) -> None: ...

quux(3, __y__=1)  # This call is fine.

quux(__x=3)  # This call is an error.

生成器函数和协程的注解

生成器函数的返回类型可以使用 typing.py 模块提供的泛型类型 Generator[yield_type, send_type, return_type] 进行注解:

def echo_round() -> Generator[int, float, str]:
    res = yield
    while res:
        res = yield round(res)
    return 'OK'

PEP 492 中引入的协程使用与普通函数相同的语法进行注解。然而,返回类型注解对应于 await 表达式的类型,而不是协程类型:

async def spam(ignored: int) -> str:
    return 'spam'

async def foo() -> None:
    bar = await spam(42)  # type: str

typing.py 模块提供了 ABC collections.abc.Coroutine 的泛型版本,以指定也支持 send()throw() 方法的可等待对象。类型变量的变体和顺序对应于 Generator 的变体和顺序,即 Coroutine[T_co, T_contra, V_co],例如:

from typing import List, Coroutine
c = None  # type: Coroutine[List[str], str, int]
...
x = c.send('hi')  # type: List[str]
async def bar() -> None:
    x = await c  # type: int

该模块还提供了泛型 ABC AwaitableAsyncIterableAsyncIterator,用于无法指定更精确类型的情况:

def op() -> typing.Awaitable[str]:
    if cond:
        return spam(42)
    else:
        return asyncio.Future(...)

与函数注解的其他用途的兼容性

存在一些现有或潜在的函数注解用例,它们与类型提示不兼容。这可能会使静态类型检查器感到困惑。然而,由于类型提示注解没有运行时行为(除了注解表达式的求值和将注解存储在函数对象的 __annotations__ 属性中),这并不会使程序不正确——它只会导致类型检查器发出虚假的警告或错误。

要标记不应受类型提示覆盖的程序部分,你可以使用以下一个或多个:

  • 一个 # type: ignore 注释;
  • 一个应用于类或函数的 @no_type_check 装饰器;
  • 一个用 @no_type_check_decorator 标记的自定义类或函数装饰器。

有关更多详细信息,请参阅后面的章节。

为了最大程度地兼容离线类型检查,最终最好改变依赖注解的接口,转而使用不同的机制,例如装饰器。然而,在 Python 3.5 中没有必要这样做。另请参阅下面的 被拒绝的替代方案 中的更长篇讨论。

类型注释

本 PEP 没有添加显式标记变量为特定类型的一等语法支持。为了在复杂情况下帮助类型推断,可以使用以下格式的注释:

x = []                # type: List[Employee]
x, y, z = [], [], []  # type: List[int], List[int], List[str]
x, y, z = [], [], []  # type: (List[int], List[int], List[str])
a, b, *c = range(5)   # type: float, float, List[float]
x = [1, 2]            # type: List[int]

类型注释应放在包含变量定义的语句的最后一行。它们也可以放在 with 语句和 for 语句中,紧跟在冒号之后。

withfor 语句上的类型注释示例:

with frobnicate() as foo:  # type: int
    # Here foo is an int
    ...

for x, y in points:  # type: float, float
    # Here x and y are floats
    ...

在存根文件中,声明变量的存在而不赋予其初始值可能很有用。这可以通过 PEP 526 变量注解语法来完成:

from typing import IO

stream: IO[str]

上述语法在所有 Python 版本的存根文件中都是可接受的。然而,在 Python 3.5 及更早版本的非存根代码中,存在一个特殊情况:

from typing import IO

stream = None  # type: IO[str]

类型检查器不应对此抱怨(尽管 None 值与给定类型不匹配),也不应将推断类型更改为 Optional[...](尽管对于默认值为 None 的注解参数有此规则)。这里的假设是其他代码将确保变量被赋予正确类型的值,并且所有使用都可以假定变量具有给定类型。

# type: ignore 注释应放在错误所指的行上:

import http.client
errors = {
    'not_found': http.client.NOT_FOUND  # type: ignore
}

在文件顶部,任何文档字符串、导入或其他可执行代码之前,单独一行上的 # type: ignore 注释会抑制文件中所有错误。空行和其他注释,如 shebang 行和编码 cookie,可以位于 # type: ignore 注释之前。

在某些情况下,可能需要在与类型注释同一行上进行 linting 工具或其他注释。在这些情况下,类型注释应位于其他注释和 linting 标记之前:

# type: ignore # <注释或其他标记>

如果类型提示普遍有用,未来的 Python 版本可能会提供变量类型化的语法。(更新:此语法已在 Python 3.6 中通过 PEP 526 添加。)

类型转换

有时类型检查器可能需要不同类型的提示:程序员可能知道一个表达式的类型比类型检查器能够推断的更受限制。例如:

from typing import List, cast

def find_first_str(a: List[object]) -> str:
    index = next(i for i, x in enumerate(a) if isinstance(x, str))
    # We only get here if there's at least one string in a
    return cast(str, a[index])

有些类型检查器可能无法推断出 a[index] 的类型是 str,而只能推断出 objectAny,但我们知道(如果代码到达该点)它必须是一个字符串。cast(t, x) 调用告诉类型检查器我们确信 x 的类型是 t。在运行时,cast 始终返回未更改的表达式——它不检查类型,也不转换或强制转换值。

类型转换与类型注释(参见上一节)不同。使用类型注释时,类型检查器仍应验证推断类型与声明类型一致。使用类型转换时,类型检查器应盲目相信程序员。此外,类型转换可以在表达式中使用,而类型注释仅适用于赋值。

NewType 辅助函数

在某些情况下,程序员可能希望通过创建简单的类来避免逻辑错误。例如:

class UserId(int):
    pass

def get_by_user_id(user_id: UserId):
    ...

然而,这种方法会引入运行时开销。为了避免这种情况,typing.py 提供了一个辅助函数 NewType,它创建简单的唯一类型,几乎没有运行时开销。对于静态类型检查器,Derived = NewType('Derived', Base) 大致等同于一个定义:

class Derived(Base):
    def __init__(self, _x: Base) -> None:
        ...

而在运行时,NewType('Derived', Base) 返回一个仅返回其参数的虚拟函数。类型检查器要求在预期 UserId 的地方从 int 进行显式类型转换,同时在预期 int 的地方从 UserId 进行隐式类型转换。示例:

UserId = NewType('UserId', int)

def name_by_id(user_id: UserId) -> str:
    ...

UserId('user')          # Fails type check

name_by_id(42)          # Fails type check
name_by_id(UserId(42))  # OK

num = UserId(5) + 1     # type: int

NewType 接受恰好两个参数:新唯一类型的名称和基类。基类应该是一个合适的类(即,不是像 Union 等类型构造),或者是通过调用 NewType 创建的另一个唯一类型。由 NewType 返回的函数只接受一个参数;这等同于只支持一个接受基类实例的构造函数(见上文)。例如:

class PacketId:
    def __init__(self, major: int, minor: int) -> None:
        self._major = major
        self._minor = minor

TcpPacketId = NewType('TcpPacketId', PacketId)

packet = PacketId(100, 100)
tcp_packet = TcpPacketId(packet)  # OK

tcp_packet = TcpPacketId(127, 0)  # Fails in type checker and at runtime

对于 NewType('Derived', Base)isinstanceissubclass,以及子类化都将失败,因为函数对象不支持这些操作。

存根文件

存根文件是包含类型提示的文件,仅供类型检查器使用,不在运行时使用。存根文件有几种用例:

  • 扩展模块
  • 其作者尚未添加类型提示的第三方模块
  • 尚未编写类型提示的标准库模块
  • 必须与 Python 2 和 3 兼容的模块
  • 将注解用于其他目的的模块

存根文件与常规 Python 模块具有相同的语法。在存根文件中,typing 模块的一个特性有所不同:下面描述的 @overload 装饰器。

类型检查器应只检查存根文件中的函数签名;建议存根文件中的函数体只包含一个省略号 (...)。

类型检查器应具有可配置的存根文件搜索路径。如果找到存根文件,类型检查器不应读取相应的“真实”模块。

虽然存根文件在语法上是有效的 Python 模块,但它们使用 .pyi 扩展名,以便可以将存根文件与相应的真实模块维护在同一目录中。这也强化了存根文件不应期望任何运行时行为的观念。

关于存根文件的补充说明

  • 导入到存根中的模块和变量不被视为从存根中导出,除非导入使用 import ... as ... 形式或等效的 from ... import ... as ... 形式。(更新:澄清一下,这里的意图是只有使用 X as X 形式导入的名称才会被导出,即 as 前后的名称必须相同。)
  • 但是,作为前一个要点的例外,使用 from ... import * 导入到存根中的所有对象都被视为已导出。(这使得从给定模块重新导出所有可能因 Python 版本而异的对象变得更容易。)
  • 就像在普通 Python 文件中一样,子模块在导入时会自动成为其父模块的导出属性。例如,如果 spam 包具有以下目录结构
    spam/
        __init__.pyi
        ham.pyi
    

    其中 __init__.pyi 包含诸如 from . import hamfrom .ham import Ham 的行,则 hamspam 的导出属性。

  • 存根文件可能不完整。为了让类型检查器知道这一点,文件可以包含以下代码
    def __getattr__(name) -> Any: ...
    

    因此,在存根中未定义的任何标识符都被假定为 Any 类型。

函数/方法重载

@overload 装饰器允许描述支持多种不同参数类型组合的函数和方法。这种模式在内置模块和类型中经常使用。例如,bytes 类型的 __getitem__() 方法可以描述如下

from typing import overload

class bytes:
    ...
    @overload
    def __getitem__(self, i: int) -> int: ...
    @overload
    def __getitem__(self, s: slice) -> bytes: ...

这种描述比使用联合(无法表达参数和返回类型之间的关系)更精确

from typing import Union

class bytes:
    ...
    def __getitem__(self, a: Union[int, slice]) -> Union[int, bytes]: ...

@overload 派上用场的另一个例子是内置 map() 函数的类型,它根据可调用对象的类型接受不同数量的参数

from typing import Callable, Iterable, Iterator, Tuple, TypeVar, overload

T1 = TypeVar('T1')
T2 = TypeVar('T2')
S = TypeVar('S')

@overload
def map(func: Callable[[T1], S], iter1: Iterable[T1]) -> Iterator[S]: ...
@overload
def map(func: Callable[[T1, T2], S],
        iter1: Iterable[T1], iter2: Iterable[T2]) -> Iterator[S]: ...
# ... and we could add more items to support more than two iterables

请注意,我们也可以轻松添加项目以支持 map(None, ...)

@overload
def map(func: None, iter1: Iterable[T1]) -> Iterable[T1]: ...
@overload
def map(func: None,
        iter1: Iterable[T1],
        iter2: Iterable[T2]) -> Iterable[Tuple[T1, T2]]: ...

如上所示的 @overload 装饰器的用法适用于存根文件。在常规模块中,一系列 @overload 装饰的定义后面必须紧跟一个未被 @overload 装饰的定义(对于相同的函数/方法)。@overload 装饰的定义仅供类型检查器使用,因为它们将被未被 @overload 装饰的定义覆盖,而后者在运行时使用但应被类型检查器忽略。在运行时,直接调用 @overload 装饰的函数将引发 NotImplementedError。下面是一个非存根重载的示例,它无法轻松地使用联合或类型变量来表达

@overload
def utf8(value: None) -> None:
    pass
@overload
def utf8(value: bytes) -> bytes:
    pass
@overload
def utf8(value: unicode) -> bytes:
    pass
def utf8(value):
    <actual implementation>

注意:虽然可以使用此语法提供多重分派实现,但其实现将需要使用 sys._getframe(),这是不推荐的。此外,设计和实现高效的多重分派机制很困难,这就是为什么之前的尝试都被放弃而转向 functools.singledispatch()。(请参阅 PEP 443,特别是其“替代方法”部分。)将来我们可能会提出一个令人满意的多重分派设计,但我们不希望这样的设计受到存根文件中为类型提示定义的重载语法的限制。两种功能也可能相互独立发展(因为类型检查器中的重载与运行时多重分派具有不同的用例和要求——例如,后者不太可能支持泛型类型)。

受约束的 TypeVar 类型通常可以替代 @overload 装饰器。例如,此存根文件中的 concat1concat2 的定义是等效的

from typing import TypeVar, Text

AnyStr = TypeVar('AnyStr', Text, bytes)

def concat1(x: AnyStr, y: AnyStr) -> AnyStr: ...

@overload
def concat2(x: str, y: str) -> str: ...
@overload
def concat2(x: bytes, y: bytes) -> bytes: ...

某些函数,例如上面的 mapbytes.__getitem__,无法使用类型变量精确表示。然而,与 @overload 不同,类型变量也可以在存根文件之外使用。我们建议仅在类型变量不足的情况下使用 @overload,因为它具有特殊的仅限存根状态。

诸如 AnyStr 之类的类型变量与使用 @overload 之间的另一个重要区别是,前者还可以用于定义泛型类类型参数的约束。例如,泛型类 typing.IO 的类型参数受到约束(只有 IO[str]IO[bytes]IO[Any] 是有效的)

class IO(Generic[AnyStr]): ...

存储和分发存根文件

存根文件最简单的存储和分发形式是将其与 Python 模块放在同一目录中。这使得程序员和工具都可以轻松找到它们。但是,由于包维护者可以自由选择是否为其包添加类型提示,因此也支持通过 PyPI 从 pip 安装的第三方存根。在这种情况下,我们必须考虑三个问题:命名、版本控制、安装路径。

本 PEP 未提供第三方存根文件包应使用的命名方案的建议。可发现性有望基于包的受欢迎程度,例如 Django 包。

第三方存根必须使用兼容的源包的最低版本进行版本控制。示例:FooPackage 有版本 1.0、1.1、1.2、1.3、2.0、2.1、2.2。版本 1.1、2.0 和 2.2 有 API 更改。存根文件包维护者可以自由发布所有版本的存根,但至少需要 1.0、1.1、2.0 和 2.2 才能让最终用户类型检查所有版本。这是因为用户知道最接近的 更低或相等 版本的存根是兼容的。在提供的示例中,对于 FooPackage 1.3,用户将选择存根版本 1.1。

请注意,如果用户决定使用“最新”可用的源包,那么如果存根文件经常更新,使用“最新”存根文件通常也应该有效。

第三方存根包可以使用任何位置来存储存根。类型检查器应使用 PYTHONPATH 搜索它们。一个总是检查的默认回退目录是 shared/typehints/pythonX.Y/(对于由类型检查器确定的某个 PythonX.Y,而不仅仅是已安装的版本)。由于每个环境的给定 Python 版本只能安装一个包,因此在该目录下不执行额外的版本控制(就像 pip 在 site-packages 中的裸目录安装一样)。存根文件包作者可以在 setup.py 中使用以下代码片段

...
data_files=[
    (
        'shared/typehints/python{}.{}'.format(*sys.version_info[:2]),
        pathlib.Path(SRC_PATH).glob('**/*.pyi'),
    ),
],
...

更新:截至 2018 年 6 月,分发第三方包的类型提示的推荐方式已更改——除了 typeshed(参见下一节)之外,现在还有一个分发类型提示的标准,PEP 561。它支持包含存根的单独可安装包、包含在与包的可执行代码相同的分发中的存根文件以及内联类型提示,后两个选项通过在包中包含名为 py.typed 的文件来启用。)

Typeshed 仓库

有一个共享存储库,其中收集了有用的存根。此处收集的存根的策略将单独决定并在存储库的文档中报告。请注意,如果包所有者明确要求省略给定包的存根,则此处将不包含它们。

异常

没有提出明确列出引发的异常的语法。目前,此功能唯一的已知用例是文档,在这种情况下,建议将此信息放入文档字符串中。

typing 模块

为了使静态类型检查的使用扩展到 Python 3.5 以及旧版本,需要一个统一的命名空间。为此,标准库中引入了一个新模块,名为 typing

它定义了构建类型的基本构建块(例如 Any)、表示内置集合的泛型变体的类型(例如 List)、表示泛型集合 ABC 的类型(例如 Sequence)以及一小部分便利定义。

请注意,特殊的类型构造,例如 AnyUnion 和使用 TypeVar 定义的类型变量仅在类型注解上下文支持,并且 Generic 只能用作基类。所有这些(除了未参数化的泛型)如果出现在 isinstanceissubclass 中,都将引发 TypeError

基本构建块

  • Any,用作 def get(key: str) -> Any: ...
  • Union,用作 Union[Type1, Type2, Type3]
  • Callable,用作 Callable[[Arg1Type, Arg2Type], ReturnType]
  • Tuple,通过列出元素类型使用,例如 Tuple[int, int, str]。空元组可以类型化为 Tuple[()]。任意长度的同构元组可以使用一个类型和省略号表达,例如 Tuple[int, ...]。(这里的 ... 是语法的一部分,一个字面省略号。)
  • TypeVar,用作 X = TypeVar('X', Type1, Type2, Type3) 或简单地 Y = TypeVar('Y')(有关更多详细信息,请参见上文)
  • Generic,用于创建用户定义的泛型类
  • Type,用于注解类对象

内置集合的泛型变体

  • Dict,用作 Dict[key_type, value_type]
  • DefaultDict,用作 DefaultDict[key_type, value_type],是 collections.defaultdict 的泛型变体
  • List,用作 List[element_type]
  • Set,用作 Set[element_type]。参见下面 AbstractSet 的备注。
  • FrozenSet,用作 FrozenSet[element_type]

注意:DictDefaultDictListSetFrozenSet 主要用于注解返回值。对于参数,请首选下面定义的抽象集合类型,例如 MappingSequenceAbstractSet

容器 ABC(和一些非容器)的泛型变体

  • Awaitable
  • AsyncIterable
  • AsyncIterator
  • ByteString
  • Callable(见上文,此处列出以供完整性)
  • Collection
  • Container
  • ContextManager
  • Coroutine
  • Generator,用作 Generator[yield_type, send_type, return_type]。这表示生成器函数的返回值。它是 Iterable 的子类型,它具有额外的类型变量,用于 send() 方法接受的类型(它在此变量中是逆变的——一个接受发送 Employee 实例的生成器在一个需要接受发送 Manager 实例的生成器的上下文中是有效的)和生成器的返回类型。
  • Hashable(非泛型,但此处列出以供完整性)
  • ItemsView
  • Iterable
  • Iterator
  • KeysView
  • Mapping
  • MappingView
  • MutableMapping
  • MutableSequence
  • MutableSet
  • Sequence
  • Set,重命名为 AbstractSet。此名称更改是必需的,因为 typing 模块中的 Set 表示带有泛型的 set()
  • Sized(非泛型,但此处列出以供完整性)
  • ValuesView

定义了一些用于测试单个特殊方法的一次性类型(类似于 HashableSized

  • Reversible,用于测试 __reversed__
  • SupportsAbs,用于测试 __abs__
  • SupportsComplex,用于测试 __complex__
  • SupportsFloat,用于测试 __float__
  • SupportsInt,用于测试 __int__
  • SupportsRound,用于测试 __round__
  • SupportsBytes,用于测试 __bytes__

便利定义

  • Optional,由 Optional[t] == Union[t, None] 定义
  • Text,Python 3 中 str 的简单别名,Python 2 中 unicode 的简单别名
  • AnyStr,定义为 TypeVar('AnyStr', Text, bytes)
  • NamedTuple,用作 NamedTuple(type_name, [(field_name, field_type), ...]),等效于 collections.namedtuple(type_name, [field_name, ...])。这对于声明命名元组类型的字段类型很有用。
  • NewType,用于创建运行时开销很小的唯一类型 UserId = NewType('UserId', int)
  • cast(),前面已描述
  • @no_type_check,一个装饰器,用于禁用按类或函数进行的类型检查(见下文)
  • @no_type_check_decorator,一个装饰器,用于创建您自己的具有与 @no_type_check 相同含义的装饰器(见下文)
  • @type_check_only,一个仅在类型检查期间在存根文件中可用的装饰器(见上文);将类或函数标记为在运行时不可用
  • @overload,前面已描述
  • get_type_hints(),一个实用函数,用于从函数或方法中检索类型提示。给定一个函数或方法对象,它返回一个字典,其格式与 __annotations__ 相同,但会在原始函数或方法定义的上下文中评估前向引用(以字符串文字形式给出)作为表达式。
  • TYPE_CHECKING,在运行时为 False,但对类型检查器为 True

I/O 相关类型

  • IO(AnyStr 的泛型)
  • BinaryIO(IO[bytes] 的简单子类型)
  • TextIO(IO[str] 的简单子类型)

与正则表达式和 re 模块相关的类型

  • Match 和 Pattern,re.match()re.compile() 结果的类型(AnyStr 的泛型)

Python 2.7 和跨代码的建议语法

某些工具可能希望在必须与 Python 2.7 兼容的代码中支持类型注解。为此,本 PEP 提出了一个建议(但非强制性)的扩展,其中函数注解放置在 # type: 注释中。此类注释必须紧跟在函数头之后(在文档字符串之前)。例如:以下 Python 3 代码

def embezzle(self, account: str, funds: int = 1000000, *fake_receipts: str) -> None:
    """Embezzle funds from account using fake receipts."""
    <code goes here>

等效于以下内容

def embezzle(self, account, funds=1000000, *fake_receipts):
    # type: (str, int, *str) -> None
    """Embezzle funds from account using fake receipts."""
    <code goes here>

请注意,对于方法,self 不需要类型。

对于无参数方法,它将如下所示

def load_cache(self):
    # type: () -> bool
    <code>

有时您想为函数或方法指定返回类型,而无需(或尚未)指定参数类型。为了明确支持这一点,参数列表可以用省略号替换。示例

def send_email(address, sender, cc, bcc, subject, body):
    # type: (...) -> bool
    """Send an email message.  Return True if successful."""
    <code>

有时您有很长的参数列表,并且在一个 # type: 注释中指定它们的类型会很尴尬。为此,您可以每行列出一个参数,并在参数关联的逗号(如果有)之后每行添加一个 # type: 注释。要指定返回类型,请使用省略号语法。指定返回类型不是强制性的,并且并非每个参数都需要给定类型。带有 # type: 注释的行应仅包含一个参数。最后一个参数(如果有)的类型注释应在右括号之前。示例

def send_email(address,     # type: Union[str, List[str]]
               sender,      # type: str
               cc,          # type: Optional[List[str]]
               bcc,         # type: Optional[List[str]]
               subject='',
               body=None    # type: List[str]
               ):
    # type: (...) -> bool
    """Send an email message.  Return True if successful."""
    <code>

备注

  • 支持此语法的工具应无论正在检查的 Python 版本如何都支持它。这对于支持跨 Python 2 和 Python 3 的代码是必需的。
  • 不允许参数或返回值同时具有类型注解和类型注释。
  • 使用短形式时(例如 # type: (str, int) -> None),必须考虑每个参数,除了实例和类方法的第一个参数(这些通常被省略,但允许包含它们)。
  • 短形式的返回类型是强制性的。如果在 Python 3 中您会省略某些参数或返回类型,则 Python 2 符号应使用 Any
  • 使用短形式时,对于 *args**kwds,在相应的类型注解前面加上 1 或 2 个星号。(与 Python 3 注解一样,这里的注解表示单个参数值的类型,而不是您作为特殊参数值 argskwds 接收的元组/字典的类型。)
  • 与其他类型注释一样,注解中使用的任何名称都必须由包含注解的模块导入或定义。
  • 使用短形式时,整个注解必须在一行中。
  • 短形式也可以与右括号在同一行,例如
    def add(a, b):  # type: (int, int) -> int
        return a + b
    
  • 放置错误的类型注释将被类型检查器标记为错误。如有必要,此类注释可以进行两次注释。例如
    def f():
        '''Docstring'''
        # type: () -> None  # Error!
    
    def g():
        '''Docstring'''
        # # type: () -> None  # This is OK
    

检查 Python 2.7 代码时,类型检查器应将 intlong 类型视为等效。对于类型为 Text 的参数,类型为 strunicode 的参数都应可接受。

被拒绝的替代方案

在本 PEP 的早期草案讨论期间,提出了各种反对意见和替代方案。我们在此讨论其中一些并解释我们为何拒绝它们。

提出了几个主要反对意见。

泛型类型参数使用哪种括号?

大多数人熟悉在 C++、Java、C# 和 Swift 等语言中使用尖括号(例如 List<int>)来表达泛型类型的参数化。这些的问题是它们真的很难解析,特别是对于像 Python 这样简单的解析器。在大多数语言中,通常通过只允许在不允许一般表达式的特定句法位置使用尖括号来解决歧义。(也通过使用非常强大的解析技术,可以回溯任意代码段。)

但在 Python 中,我们希望类型表达式在语法上与其他表达式相同,这样我们就可以使用例如变量赋值来创建类型别名。考虑这个简单的类型表达式

List<int>

从 Python 解析器的角度来看,表达式以与链式比较相同的四个标记(NAME, LESS, NAME, GREATER)开头

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 中的泛型类也使用方括号。

现有注解的使用情况如何?

一个论点指出,PEP 3107 明确支持在函数注解中使用任意表达式。因此,新提案被认为与 PEP 3107 的规范不兼容。

我们对此的回应是,首先,当前提案没有引入任何直接不兼容性,因此在 Python 3.4 中使用注解的程序在 Python 3.5 中仍将正确且不受影响地工作。

我们确实希望类型提示最终将成为注解的唯一用途,但这需要在 Python 3.5 发布 typing 模块之后进行额外的讨论和弃用期。当前 PEP 将具有临时状态(参见 PEP 411),直到 Python 3.6 发布。最快可行的方案将在 3.6 中引入非类型提示注解的静默弃用,在 3.7 中完全弃用,并在 Python 3.8 中声明类型提示为注解的唯一允许用途。即使类型提示一夜成功,这应该给使用注解的包的作者足够的时间来设计另一种方法。

更新:截至 2017 年秋季,本 PEP 和 typing.py 模块的临时状态结束时间表已更改,注解的其他用途的弃用时间表也已更改。有关更新的时间表,请参见 PEP 563。)

另一个可能的结果是类型提示最终将成为注解的默认含义,但始终会有一个选项来禁用它们。为此,当前提案定义了一个装饰器 @no_type_check,它在给定的类或函数中禁用注解作为类型提示的默认解释。它还定义了一个元装饰器 @no_type_check_decorator,它可用于装饰一个装饰器(!),从而使任何用后者装饰的函数或类中的注解被类型检查器忽略。

还有 # type: ignore 注释,静态检查器应支持配置选项以禁用所选包中的类型检查。

尽管有所有这些选项,但仍有人提出了允许类型提示和其他形式的注解在单个参数上共存的提案。一个提案建议,如果给定参数的注解是一个字典字面量,则每个键代表一种不同形式的注解,并且键 'type' 将用于类型提示。这个想法及其变体的问题在于,符号变得非常“嘈杂”且难以阅读。此外,在大多数现有库使用注解的情况下,几乎没有必要将它们与类型提示结合起来。因此,选择性禁用类型提示的更简单方法似乎就足够了。

前向声明的问题

当前提案在类型提示必须包含前向引用时确实是次优的。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__ import 语句可能会在单独的 PEP 中提出。

更新:__future__ import 语句及其后果在 PEP 563 中讨论。)

双冒号

一些有创意的人试图为这个问题发明解决方案。例如,有人建议使用双冒号(::)作为类型提示,一次解决两个问题:区分类型提示和其他注解,并改变语义以排除运行时评估。然而,这个想法有几个问题。

  • 它很难看。Python 中的单冒号有很多用途,所有这些都看起来很熟悉,因为它们类似于英语文本中冒号的用法。这是 Python 遵守大多数标点符号形式的一般经验法则;例外通常在其他编程语言中广为人知。但是 :: 的这种用法在英语中闻所未闻,在其他语言(例如 C++)中,它被用作作用域运算符,这是一个非常不同的东西。相比之下,类型提示的单冒号读起来很自然——这不足为奇,因为它正是为此目的而精心设计的(这个想法早于PEP 3107)。它在 Pascal 到 Swift 等其他语言中也以相同的方式使用。
  • 您将如何处理返回类型注解?
  • 事实上,类型提示在运行时进行评估是一个特性。
    • 在运行时提供类型提示允许在类型提示之上构建运行时类型检查器。
    • 即使类型检查器未运行,它也会捕获错误。由于它是一个单独的程序,用户可以选择不运行它(甚至不安装它),但可能仍希望使用类型提示作为一种简洁的文档形式。损坏的类型提示即使对于文档也毫无用处。
  • 因为它是新语法,使用双冒号进行类型提示会将其限制为仅适用于 Python 3.5 的代码。通过使用现有语法,当前提案可以轻松地适用于较旧的 Python 3 版本。(实际上 mypy 支持 Python 3.2 及更高版本。)
  • 如果类型提示成功,我们将来很可能会决定添加新语法来声明变量的类型,例如 var age: int = 42。如果我们要对参数类型提示使用双冒号,为了保持一致性,我们必须对未来语法使用相同的约定,从而使丑陋永久化。

其他新语法形式

还提出了其他一些替代语法形式,例如 引入一个 where 关键字,以及受 Cobra 启发的 requires 子句。但这些都与双冒号有一个共同的问题:它们不适用于较早的 Python 3 版本。新的 __future__ import 也会遇到同样的情况。

其他向后兼容的约定

提出的想法包括

  • 一个装饰器,例如 @typehints(name=str, returns=str)。这可能有效,但它非常冗长(额外的一行,并且参数名称必须重复),而且与 PEP 3107 符号的优雅相去甚远。
  • 存根文件。我们确实需要存根文件,但它们主要用于向不适合添加类型提示的现有代码添加类型提示,例如第三方包、需要支持 Python 2 和 Python 3 的代码,尤其是扩展模块。在大多数情况下,将注解与函数定义内联在一起使它们更有用。
  • 文档字符串。文档字符串有一个现有的约定,基于 Sphinx 符号(:type arg1: description)。这非常冗长(每个参数额外一行),而且不太优雅。我们也可以创造一些新的东西,但注解语法很难超越(因为它正是为此目的而设计的)。

也有人建议简单地再等一个版本。但这会解决什么问题呢?这只会是拖延。

PEP 开发过程

本 PEP 的实时草案位于 GitHub 上。还有一个问题跟踪器,大部分技术讨论都在那里进行。

GitHub 上的草案会定期小幅更新。官方 PEPS 存储库(通常)只在新草案发布到 python-dev 时更新。

致谢

本文件无法在没有 Jim Baker、Jeremy Siek、Michael Matson Vitousek、Andrey Vlasovskikh、Radomir Dopieralski、Peter Ludemann 以及 BDFL 委托 Mark Shannon 的宝贵投入、鼓励和建议下完成。

影响因素包括 PEP 482 中提到的现有语言、库和框架。非常感谢它们的创建者,按字母顺序排列:Stefan Behnel、William Edwards、Greg Ewing、Larry Hastings、Anders Hejlsberg、Alok Menghrajani、Travis E. Oliphant、Joe Pamer、Raoul-Gabriel Urma 和 Julien Verlaguet。


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

最后修改:2025-02-01 08:59:27 GMT