PEP 484 – 类型提示

摘要

PEP 3107引入了函数注释的语法,但故意未定义语义。现在已经有足够多的第三方使用静态类型分析,社区将从标准库中的标准词汇表和基线工具中受益。

此 PEP 引入了一个临时模块来提供这些标准定义和工具,以及一些用于注释不可用的情况的约定。

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

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

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

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

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

类型系统支持联合、泛型类型和Any与所有类型一致(即可分配给和从)命名的特殊类型。后一个特征取自渐进类型的思想。PEP 483中解释了渐进类型和完整类型系统。

PEP 482中描述了我们借用的其他方法或可以与之比较和对比的其他方法。

基本原理和目标

PEP 3107添加了对函数定义部分的任意注释的支持。尽管当时没有为注释分配任何意义,但始终有一个隐含的目标是使用它们进行类型提示[gvr-artima],这在所述 PEP 中被列为第一个可能的用例。

此 PEP 旨在为类型注释提供标准语法,开放 Python 代码以更轻松地进行静态分析和重构、潜在的运行时类型检查以及(可能在某些情况下)利用类型信息的代码生成。

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

非目标

虽然提议的类型模块将包含一些用于运行时类型检查的构建块——特别是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 的未来版本中添加特定的包含和排除项。)

除了上述之外,还可以使用下面定义的以下特殊结构:NoneAnyUnionTuple、 、所有 ABC 和从(例如和)Callable导出的具体类的替代项、类型变量和类型别名。typing``Sequence``Dict

用于支持以下部分中描述的功能的所有新引入的名称(例如AnyUnion)在typing模块中可用。

使用无

当在类型提示中使用时,表达式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: ...

泛型可以通过使用被调用的新工厂来参数typingTypeVar。例子:

from typing import Sequence, TypeVar

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

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

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

必须始终将TypeVar()表达式直接分配给变量(不应将其用作较大表达式的一部分)。to 的参数TypeVar()必须是与分配给它的变量名称相同的字符串。不得重新定义类型变量。

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

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将设置为strand not 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
    
    

实例化泛型类和类型擦除

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

from typing import TypeVar, Generic

T = TypeVar('T')

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

要创建Node实例,您Node()可以像调用普通类一样调用。在运行时,实例的类型(类)将为Node. 但它对类型检查器有什么类型?答案取决于通话中有多少信息可用。如果构造函数 ( __init__or __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

注意pand的运行时类型(类)q仍然只是Node-and是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

抽象集合的通用版本,如MappingorSequence和内置类的通用版本 - 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

SymbolTable是 的子类dict和子类型。Dict[str, List[Node]]

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

from typing import TypeVar, Iterable, Container

T = TypeVar('T')

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

NowLinkedList[int]是一个有效的类型。注意,我们可以在基类列表中多次使用,T只要我们不在.T``Generic[...]

还要考虑以下示例:

from typing import TypeVar, Mapping

T = TypeVar('T')

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

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

抽象泛型类型

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

键入具有上限的变量

类型变量可以使用指定上限bound=<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]

上限不能与类型约束结合使用(如 used AnyStr,参见前面的示例);类型约束导致推断类型是_exactly_约束类型之一,而上限只要求实际类型是边界类型的子类型。

协变和逆变

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

事实证明,这样的参数是_逆变_的,而直观的答案(如果函数不改变其参数,这是正确的!)要求参数是_协变_的。更多关于这些概念的介绍可以在 Wikipedia [wiki-variance]PEP 483中找到;这里我们只展示如何控制类型检查器的行为。

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

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

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

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 的数字塔,stdlib 模块numbers实现了相应的 ABC(、NumberComplexReal)。这些 ABC 存在一些问题,但内置的具体数字类和无处不在(尤其是后两个 😃。Rational``Integral``complex``float``int

这个 PEP没有要求用户编写然后使用等,而是提出了一个几乎同样有效的简单快捷方式:当一个参数被注释为具有 type时,一个 type 的参数是可以接受的;类似地,对于注释为具有 type的参数,类型或参数是可接受的。这不处理实现相应 ABC 或类的类,但我们相信这些用例极为罕见。import numbers``numbers.Float``float``int``complex``float``int``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 中的行出现 ImportError 失败,该行是在 a 定义类 A 之前从 models/a.py 导入的。解决方案是切换到仅模块导入和通过它们的_module_._class_名称引用模型:from models.a import 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, ...]``T1``T2``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,类型检查器将拒绝对其进行的几乎所有操作,并将其分配给更特殊类型的变量(或将其用作返回值)是类型错误。另一方面,当一个值具有 typeAny时,类型检查器将允许对其进行所有操作,并且Any可以将 type 的值分配给更受约束类型的变量(或用作返回值)。

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

from typing import Mapping

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

此规则也适用于Tuple,在注释上下文中,它等价于,反过来,也适用于。同样,注解中的一个bare 等价于,反过来,也等价于: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]where Cis a class。澄清一下:while C(当用作注释时)指的是 class 的实例_,_CType[C]的是. (这与和之间的区别类似。)C``object``type

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

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

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

joe = new_user(BasicUser)  # Inferred type is BasicUser

对应的值Type[C]必须是一个实际的类对象,它是 的子类型C,而不是特殊形式。换句话说,在上面的例子中调用 eg被类型检查器拒绝(除了在运行时失败,因为你不能实例化一个联合)。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注释类方法的第一个参数时允许使用类型变量(参见相关部分)。

任何其他特殊构造,如TupleorCallable不允许作为Type.

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

Type被参数化时,它只需要一个参数。Type不带括号的普通等价于Type[Any],而这又等价于type(Python 的元类层次结构的根)。这种等效性也激发了名称 , Type,而不是替代品Classor SubType,后者在讨论此功能时提出;这类似于 egList和之间的关系list

关于Type[Any](or Typeor type) 的行为,访问具有这种类型的变量的属性仅提供由type(例如__repr__()and __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_CHECKINGTrue在类型检查(或其他静态分析)期间但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)

在函数体中fooargs推导出变量的类型为 ,变量的类型为。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.

注释生成器函数和协程

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

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模块提供了一个通用版本的 ABCcollections.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

该模块还提供了通用 ABCs Awaitable, AsyncIterable, 和AsyncIterator用于无法指定更精确类型的情况:

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 中没有这样做的压力。另请参阅下面被拒绝的替代方案下的更长讨论。

演员表

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

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]is的类型,str而只能推断出objector Any,但我们知道(如果代码到达那一点)它必须是一个字符串。该调用告诉类型检查器我们确信 的类型是。在运行时,强制转换总是返回不变的表达式——它不检查类型,也不转换或强制转换值。cast(t, x)``x``t

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

NewType 辅助函数

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

class UserId(int):
    pass

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)``int``UserId``UserId``int

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

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

存根文件

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

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

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

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

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

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

关于存根文件的附加说明:

  • 导入到存根中的模块和变量不被视为从存根中导出,除非导入使用形式或等效形式。(_更新:_为了澄清,这里的意图是只导出使用表单导入的名称,即之前和之后的名称必须相同。)import ... as ...``from ... import ... as ...``X as X``as

  • 但是,作为上一个项目符号的例外,所有导入到存根中的对象都被视为已导出。(这使得从给定模块重新导出可能因 Python 版本而异的所有对象变得更加容易。)from ... import *

  • 就像在普通的 Python 文件[importdocs]中一样,子模块在导入时会自动成为其父模块的导出属性。例如,如果spam包具有以下目录结构:

    spam/
        __init__.pyi
        ham.pyi
    
    

    where__init__.pyi包含一行,例如or ,然后是 的导出属性。from . import ham``from .ham import Ham``ham``spam

  • 存根文件可能不完整。为了让类型检查器意识到这一点,该文件可以包含以下代码:

    def __getattr__(name) -> Any: ...
    
    

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

函数/方法重载

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

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-decorated 定义必须紧跟一个非@overload-decorated 定义(对于相同的函数/方法)。-decorated@overload定义仅用于类型检查器,因为它们将被非@overload-decorated 定义覆盖,而后者在运行时使用但应被类型检查器忽略。在运行时,直接调用@overload-decorated 函数将引发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>

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

TypeVar通常可以使用受约束的类型而不是使用@overload装饰器。例如,这个存根文件中concat1和的定义是等价的:concat2

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类型变量和 using之间的另一个重要区别@overload是先验也可用于定义泛型类类型参数的约束。例如,泛型类的类型参数typing.IO是受约束的(只有IO[str],IO[bytes]IO[Any]是有效的):

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

存储和分发存根文件

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

此 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 存储库

有一个共享存储库正在收集有用的存根[typeshed]。有关此处收集的存根的政策将单独决定,并在 repo 的文档中报告。请注意,如果包所有者特别要求省略给定包的存根,则此处不会包含它们。

例外

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

typing模块_

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

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

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

基本构建块:

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

内置集合的通用变体:

  • 字典,用作Dict[key_type, value_type]
  • DefaultDict,用作的通用变体DefaultDict[key_type, value_type]``collections.defaultdict
  • 列表,用作List[element_type]
  • 设置,用作Set[element_type]AbstractSet见下文备注。
  • FrozenSet,用作FrozenSet[element_type]

注意:DictDefaultDictListSet主要FrozenSet用于注释返回值。对于参数,更喜欢下面定义的抽象集合类型,例如MappingSequenceAbstractSet

容器 ABC 的通用变体(以及一些非容器):

  • 等待
  • 异步可迭代
  • 异步迭代器
  • 字节串
  • 可调用(见上文,此处列出以确保完整性)
  • 收藏
  • 容器
  • 上下文管理器
  • 协程
  • 生成器,用作. 这表示生成器函数的返回值。它是的子类型,并且它具有方法接受的类型的附加类型变量(它在此变量中是逆变的 - 接受发送它的实例的生成器在需要接受发送它的实例的生成器的上下文中是有效的)和生成器的返回类型。Generator[yield_type, send_type, return_type]``Iterable``send()``Employee``Manager
  • Hashable(不是通用的,但为了完整性而存在)
  • 项目查看
  • 可迭代
  • 迭代器
  • 键视图
  • 映射
  • 映射视图
  • 可变映射
  • 可变序列
  • 可变集
  • 顺序
  • 集,重命名为AbstractSet. 此名称更改是必需的,因为Settyping模块中意味着set()泛型。
  • 大小(不是通用的,但为了完整性而存在)
  • 价值观视图

定义了一些一次性类型来测试单个特殊方法(类似于Hashableor Sized):

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

方便定义:

  • 可选的,由Optional[t] == Union[t, None]
  • str文本, Python 3 中的简单别名,unicodePython 2中的 for
  • 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(),一个实用函数,用于从函数或方法中检索类型提示。给定一个函数或方法对象,它返回一个格式与 相同的 dict __annotations__,但在原始函数或方法定义的上下文中将前向引用(以字符串文字给出)作为表达式进行计算。
  • TYPE_CHECKING,False在运行时True用于类型检查器

I/O 相关类型:

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

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

  • 匹配和模式,类型re.match()re.compile()结果(泛型AnyStr

Python 2.7 和跨界代码的建议语法

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

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.

  • 使用简写形式时,对于*argsand **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的参数,类型str为 as的参数unicode应该是可以接受的。

被拒绝的替代品

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

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

泛型类型参数的括号?

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

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

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

a < b > c  # I.e., (a < b) and (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 首次推出类型模块后进行额外的讨论和弃用期。在Python 3.6 发布之前,当前的 PEP 将具有临时状态(参见PEP 411 )。可以想到的最快方案将在 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__进口声明可能会在单独的 PEP 中提出。

(_更新:_该__future__import 语句及其后果在PEP 563中进行了讨论。)

双冒号

一些有创造力的灵魂试图为这个问题发明解决方案。例如,有人提议使用双冒号 ( ::) 表示类型提示,同时解决两个问题:消除类型提示和其他注释之间的歧义,以及更改语义以排除运行时评估。然而,这个想法有几个问题。

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

其他形式的新语法

已经提出了一些其他形式的替代语法,例如引入where关键字[roberge]和受 Cobra 启发的requires子句。但是这些都与双冒号有一个问题:它们不适用于早期版本的 Python 3。同样适用于新的__future__导入。

其他向后兼容的约定

提出的想法包括:

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

也有人建议只是等待另一个版本。但这能解决什么问题呢?只会拖延。

PEP 开发过程

此 PEP 的实时草稿位于 GitHub [github]上。还有一个问题跟踪器[issues],大部分技术讨论都在这里进行。

GitHub 上的草稿会以小幅增量定期更新。官方 PEPS 存储库 [ 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。

参考

[ mypy ]

http://mypy-lang.org

[gvr-artima] ( 1 , 2 )

http://www.artima.com/weblogs/viewpost.jsp?thread=85551

[ 维基变异]

http://en.wikipedia.org/wiki/Covariance_and_contravariance_%28computer_science%29

[ 排版]

https://github.com/python/typeshed/

[pyflakes]

https://github.com/pyflakes/pyflakes/

[皮林特]

http://www.pylint.org

[ 罗伯格]

http://aroberge.blogspot.com/2015/01/type-hinting-in-python-focus-on.html

[ github ]

https://github.com/python/typing

[ 问题]

https://github.com/python/typing/issues

[打气]

https://hg.python.org/peps/file/tip/pep-0484.txt

[ 导入文档]

https://docs.python.org/3/reference/import.html#submodules

版权

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

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