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__
属性在运行时获得,但**运行时不会发生任何类型检查**。相反,该提案假设存在一个单独的离线类型检查器,用户可以自愿在其源代码上运行它。本质上,这样的类型检查器充当一个非常强大的代码风格检查器。(虽然当然可以由单个用户在运行时使用类似的检查器进行契约式设计或 JIT 优化,但这些工具尚未成熟。)
该提案受到 mypy 的强烈启发。例如,类型“整数序列”可以写成 Sequence[int]
。方括号意味着不需要向语言添加新的语法。此处的示例使用自定义类型 Sequence
,它从纯 Python 模块 typing
中导入。 Sequence[int]
符号在运行时通过在元类中实现 __getitem__()
来工作(但其意义主要在于离线类型检查器)。
类型系统支持联合类型、泛型类型和一个名为 Any
的特殊类型,它与所有类型一致(即可分配给和来自所有类型)。后一种特性取自渐进式类型化的思想。渐进式类型化和完整的类型系统在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 的未来版本中。)
除了以上内容外,还可以使用以下定义的特殊结构:None
、Any
、Union
、Tuple
、Callable
,所有 ABC 和来自 typing
的具体类的替身(例如 Sequence
和 Dict
),类型变量和类型别名。
所有新引入的用于支持以下章节中描述的功能的名称(例如 Any
和 Union
)都可以在 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
支持将参数类型约束到一组固定的可能类型(注意:这些类型不能由类型变量参数化)。例如,我们可以定义一个仅在 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
参数调用,但不能混合使用 str
和 bytes
参数。
如果有任何约束,则至少应有两个约束;不允许指定单个约束。
类型变量约束的类型的子类型应在类型变量的上下文中被视为其各自明确列出的基类型。考虑此示例
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
请注意,p
和 q
的运行时类型(类)仍然只是 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
抽象集合(如 Mapping
或 Sequence
)的泛型版本以及内置类的泛型版本——List
、Dict
、Set
和 FrozenSet
——不能被实例化。但是,它们的具体用户定义子类和具体集合的泛型版本可以被实例化。
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]):
...
现在 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
,请参阅前面的示例);类型约束导致推断的类型 _正好_ 是约束类型之一,而上限仅要求实际类型是边界类型的子类型。
协变和逆变
考虑一个具有子类 Manager
的类 Employee
。现在假设我们有一个函数,其参数用 List[Employee]
进行注释。我们是否应该允许使用类型为 List[Manager]
的变量作为其参数来调用此函数?许多人会毫不犹豫地回答“是的,当然”,甚至不考虑后果。但除非我们了解有关该函数的更多信息,否则类型检查器应该拒绝此类调用:该函数可能会将一个 Employee
实例附加到列表中,这将违反调用方中变量的类型。
事实证明,此类参数的行为是逆变的,而直观的答案(在函数不修改其参数的情况下是正确的!)要求参数的行为是协变的。可以在 Wikipedia 和 PEP 483 中找到对这些概念的更详细介绍;在这里,我们只展示如何控制类型检查器的行为。
默认情况下,泛型类型在所有类型变量中都被认为是不变的,这意味着使用类似 List[Employee]
的类型注释的变量的值必须与类型注释完全匹配 - 不允许类型参数(在此示例中为 Employee
)的子类或超类。
为了便于声明在协变或逆变类型检查可接受的容器类型,类型变量接受关键字参数 covariant=True
或 contravariant=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
中的只读集合类在其类型变量中都声明为协变(例如 Mapping
和 Sequence
)。可变集合类(例如 MutableMapping
和 MutableSequence
)声明为不变的。逆变类型的一个示例是 Generator
类型,它在 send()
参数类型中是逆变的(见下文)。
注意:协变或逆变不是类型变量的属性,而是使用此变量定义的泛型类的属性。方差仅适用于泛型类型;泛型函数不具有此属性。后者应仅使用没有 covariant
或 contravariant
关键字参数的类型变量来定义。例如,以下示例没问题
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(Number
、Complex
、Real
、Rational
和 Integral
)。这些 ABC 存在一些问题,但内置的具体数值类 complex
、float
和 int
随处可见(尤其是后两者 :-)。
此 PEP 建议使用一种简单的快捷方式,而不是要求用户编写 import numbers
然后使用 numbers.Float
等,这种快捷方式几乎同样有效:当参数被注释为具有类型 float
时,类型为 int
的参数是可以接受的;类似地,对于注释为具有类型 complex
的参数,类型为 float
或 int
的参数是可以接受的。这不会处理实现相应 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 中的 from models.a import A
行引发 ImportError,该行在 models/a.py 导入 models/b.py 之前,而 a 尚未定义类 A。解决方案是切换到仅模块导入并通过其 _module_._class_ 名称引用模型
# 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
时,类型检查器将拒绝对其进行几乎所有操作,并将它赋值给更专业类型的变量(或用作返回值)将是类型错误。另一方面,当值类型为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
的子类。(这与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:
...
现在,当我们使用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
是类型变量)在注释类方法的第一个参数时是允许的(请参阅相关部分)。
任何其他特殊构造,如Tuple
或Callable
,都不允许作为Type
的参数。
此功能存在一些问题:例如,当new_user()
调用user_class()
时,这意味着User
的所有子类都必须在其构造函数签名中支持此功能。但是,这并非Type[]
独有:类方法也有类似的问题。类型检查器应该标记此类假设的违规情况,但默认情况下,与指示的基类(上例中的User
)中的构造函数签名匹配的构造函数调用应该被允许。包含复杂或可扩展类层次结构的程序也可以通过使用工厂类方法来处理此问题。此 PEP 的未来修订版可能会引入更好的方法来解决这些问题。
当Type
被参数化时,它需要恰好一个参数。没有括号的普通Type
等效于Type[Any]
,而这又等效于type
(Python 元类层次结构的根)。这种等价性也促使了名称Type
,而不是诸如Class
或SubType
之类的替代方案,这些方案是在此功能正在讨论时提出的;这类似于例如List
和list
之间的关系。
关于Type[Any]
(或Type
或type
)的行为,访问具有此类型的变量的属性只会提供由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 Awaitable
、AsyncIterable
和 AsyncIterator
,用于无法指定更精确类型的场景。
def op() -> typing.Awaitable[str]:
if cond:
return spam(42)
else:
return asyncio.Future(...)
与函数注解的其他用途的兼容性
函数注释存在许多现有或潜在的用例,这些用例与类型提示不兼容。这些可能会混淆静态类型检查器。但是,由于类型提示注释没有运行时行为(除了评估注释表达式并将注释存储在函数对象的 __annotations__
属性中之外),因此这不会使程序出错——它只会导致类型检查器发出虚假警告或错误。
要标记程序中不应包含类型提示的部分,您可以使用以下一项或多项
- a
# type: ignore
注释; - a
@no_type_check
类或函数装饰器; - a 用
@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
语句中,紧跟在冒号后面。
在 with
和 for
语句上使用类型注释的示例
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
注释之前。
在某些情况下,可能需要在同一行上使用类型注释以及 lint 工具或其他注释。在这些情况下,类型注释应位于其他注释和 lint 标记之前。
# type: ignore # <comment or other marker>
如果类型提示通常被证明有用,则可以在未来的 Python 版本中提供用于键入变量的语法。(**更新**:此语法通过 PEP 526 在 Python 3.6 中添加。)
强制类型转换
有时,类型检查器可能需要不同类型的提示:程序员可能知道表达式的类型比类型检查器能够推断的类型更受约束。例如
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
,只能推断出 object
或 Any
,但我们知道(如果代码到达该点)它必须是字符串。该 cast(t, x)
调用告诉类型检查器我们确信 x
的类型为 t
。在运行时,强制转换始终返回未更改的表达式——它不检查类型,也不转换或强制转换值。
强制转换与类型注释不同(请参阅上一节)。使用类型注释时,类型检查器仍应验证推断的类型是否与声明的类型一致。使用强制转换时,类型检查器应盲目相信程序员。此外,强制转换可以在表达式中使用,而类型注释仅适用于赋值。
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)
返回一个虚拟函数,该函数仅返回其参数。类型检查器需要从 int
显式强制转换为预期 UserId
的地方,同时隐式从预期 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)
,isinstance
和 issubclass
以及子类化都将失败,因为函数对象不支持这些操作。
存根文件
存根文件是包含类型提示的文件,这些类型提示仅供类型检查器使用,在运行时不使用。存根文件有几个用例。
- 扩展模块
- 其作者尚未添加类型提示的第三方模块
- 尚未编写类型提示的标准库模块
- 必须与 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 ham
或from .ham import Ham
,则ham
是spam
的导出属性。 - 存根文件可能不完整。为了让类型检查器意识到这一点,文件可以包含以下代码
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
装饰器。例如,此存根文件中 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: ...
某些函数,例如上面的 map
或 bytes.__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
),以及少量便利定义。
请注意,特殊的类型构造,例如使用 TypeVar
定义的 Any
、Union
和类型变量仅在类型注释上下文中受支持,并且 Generic
只能用作基类。如果出现在 isinstance
或 issubclass
中,所有这些(除了未参数化的泛型)都将引发 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]
注意:Dict
、DefaultDict
、List
、Set
和 FrozenSet
主要用于注释返回值。对于参数,请首选下面定义的抽象集合类型,例如 Mapping
、Sequence
或 AbstractSet
。
容器 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
定义了一些一次性类型,用于测试单个特殊方法(类似于 Hashable
或 Sized
)
- 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 注释一样,此处的注释表示各个参数值的类型,而不是作为特殊参数值args
或kwds
收到的元组/字典的类型。) - 与其他类型注释一样,注释中使用的任何名称都必须由包含该注释的模块导入或定义。
- 使用简写形式时,整个注释必须在一行上。
- 简写形式也可以出现在与右括号相同的行上,例如
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 代码时,类型检查器应将 int
和 long
类型视为等效。对于类型化为 Text
的参数,类型为 str
以及 unicode
的参数都应该是可以接受的。
被拒绝的备选方案
在讨论此 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__
导入语句可能会在单独的 PEP 中提出。
(更新:该 __future__
导入语句及其后果在 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__
导入也会出现同样的情况。
其他向后兼容的约定
提出的想法包括
- 装饰器,例如
@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