英文原文:https://www.python.org/dev/peps/pep-0484/
采集日期:2019-12-27
PEP 484 -- 类型提示(Type Hints)
PEP: 484
Title: Type Hints
Author: Guido van Rossum , Jukka Lehtosalo <jukka.lehtosalo at iki.fi>, ?ukasz Langa
BDFL-Delegate: Mark Shannon
Discussions-To: Python-Dev
Status: Provisional
Type: Standards Track
Created: 29-Sep-2014
Python-Version: 3.5
Post-History: 16-Jan-2015、20-Mar-2015、17-Apr-2015、20-May-2015、22-May-2015
Resolution: https://mail.python.org/pipermail/python-dev/2015-May/140104.html
- 摘要(Abstract)
- 原由和目标(Rationale and goals)
- 注解的含义(The meaning of annotations)
- 类型定义的语法(Type definition syntax)
- 可接受的类型提示(Acceptable type hints)
None 的用法(Using None)
- 类型别名(Type aliases)
- Callable
- 泛型(Generics)
- 用户自定义的泛型类型(User-defined generic types)
- 类型变量的作用域规则(Scoping rules for type variables)
- 泛型类的实例化及类型清除(Instantiating generic classes and type erasure)
- 用任意泛型类型作为基类(Arbitrary generic types as base classes))
- 抽象泛型类型(Abstract generic types)
- 带有类型上界的类型变量(Type variables with an upper bound)
- 协变和逆变(Covariance and contravariance)
- 数值类型的继承关系(The numeric tower)
- 向前引用(Forward references)
- Union 类型(Union types)
- 用 Union 实现单实例类型的支持(Support for singleton types in unions)
Any 类型(The Any type)
NoReturn 类型(The NoReturn type)
- 类对象的类型(The type of class objects)
- 为实例和类方法加类型注解(Annotating instance and class methods)
- 版本和平台检查(Version and platform checking)
- 运行时检查还是类型检查?(Runtime or type checking?)
- 可变参数列表和默认参数值(Arbitrary argument lists and default argument values)
- 只采用位置参数(Positional-only arguments)
- 注解生成器函数和协程(Annotating generator functions and coroutines)
- 与函数注解其他用法的兼容性(
Compatibility with other uses of function annotations)
- 类型注释(Type comments)
- 指定类型(Cast)
NewType 工具函数(NewType helper function)
- 存根文件(Stub Files)
- 函数/方法重载(Function/method overloading)
- 存根文件的存储和发布(Storing and distributing stub files)
- typeshed 库(The Typeshed Repo)
- 异常(Exceptions)
- typing 模块(The typing Module)
- Python 2.7 和跨版本代码的建议语法(Suggested syntax for Python 2.7 and straddling code)
- 未被接受的替代方案(Rejected Alternatives)
- 泛型参数该用什么括号?(Which brackets for generic type parameters?)
- 已存在的注解怎么办(What about existing uses of annotations?)
- 前向声明的问题(The problem of forward declarations)
- 双冒号(The double colon)
- 其他一些新语法格式(Other forms of new syntax)
- 其他的向下兼容约定(Other backwards compatible conventions)
- PEP 开发过程(PEP Development Process)
- 致谢(Acknowledgements)
- 参考文献(References)
- 版权(Copyright)
摘要(Abstract)
PEP 3107 已经引入了函数注解(annotation)的语法,但有意将语义(semantic)保留为未定义(undefined)。目前第三方的静态类型分析应用工具已经足够多了,社区人员采用标准用语和标准库中的基线(baseline)工具就将获益良多。
为了提供标准定义和工具,本 PEP 引入了一个临时(provisional)模块,并且列出了一些不适用注解情形的约定。
请注意,即便注解符合本规范,本 PEP 依然明确不会妨碍注解的其他用法,也不要求(或禁止)对注解进行任何特殊处理。正如 PEP 333 对 Web 框架的约定,这只是为了能更好地相互合作。
比如以下这个简单的函数,其参数和返回类型都在注解给出了声明:
def greeting(name: str) -> str:
return 'Hello ' + name
虽然在运行时通过常规的 __annotations__ 属性可以访问到上述注解,但运行时并不会进行类型检查。本提案假定存在一个独立的脱机类型检查程序,用户可以自愿对源代码运行此检查程序。这种类型检查程序实质上就是一种非常强大的查错工具(linter)。当然某些用户是可以在运行时采用类似的检查程序实现“契约式设计”或JIT优化,但这些工具尚未完全成熟。
本提案受到 mypy 的强烈启发。例如,“整数序列”类型可以写为 Sequence[int]。方括号表示无需向语言添加新的语法。上述示例用到了自定义类型 Sequence,是从纯 Python 模块 typing 中导入的。通过实现元类(metaclass)中的 __getitem__() 方法,Sequence[int] 表示法在运行时得以生效(但主要是对脱机类型检查程序有意义)。
类型系统支持类型组合(Union)、泛型类型(generic type)和特殊类型 Any,Any 类型可与所有类型相容(即可以赋值给所有类型,也可以从所有类型赋值)。Any 类型的特性取自渐进定型(gradual typing)的理念。渐进定型和全类型系统已在 PEP 483 中有所解释。
在 PEP 482 中,还介绍了其他一些已借鉴或可比较的方案。
原由和目标(Rationale and Goals)
PEP 3107 已加入了为函数定义中的各个部分添加注解的支持。尽管没有为注解定义什么含义,但已经隐隐有了一个目标,即把注解用于类型提示 gvr-artima,在 PEP 3107 中这被列为第一个可能应用的场景。
本 PEP 旨在为类型注解提供一种标准语法,让 Python 代码更加开放、更易于静态分析和重构,提供一种潜在的运行时类型检查方案,以及(或许在某些上下文中)能利用类型信息生成代码。
在这些目标中,静态分析是最重要的。包括了对 mypy 这类脱机类型检查程序的支持,以及可供 IDE 使用的代码自动补全和重构的标准表示法。
非本文目标(Non-goals)
虽然本提案的 typing 模块将包含一些用于运行时类型检查的功能模块,特别是 get_type_hints() 函数,但必须开发第三方程序包才能实现特定的运行时类型检查功能,比如使用装饰器(decorator)或元类。至于如何利用类型提示进行性能优化,就留给读者当作练习吧。
还有一点应该强调一下,Python 仍将保持为一种动态类型语言,并且按惯例作者从没希望让类型提示成为强制特性。
注解的含义(The meaning of annotations)
不带注解的函数都应被视为其类型可能是最通用的,或者应被所有类型检查器忽略的。具有 @no_type_check 装饰器的函数应被视为不带注解的。
建议但不强求被检查函数的全部参数和返回类型都带有注解。被检查函数的参数和返回类型的缺省注解为 Any。不过有一种例外情况,就是实例和类方法的第一个参数。如果未带注解,则假定实例方法第一个参数的类型就是所在类(的类型),而类方法第一个参数的类型则为所在对象类(的类型)。例如,在类 A 中,实例方法第一个参数的类型隐含为 A。在类方法中,第一个参数的精确类型没法用类型注解表示。
请注意,__init__ 的返回类型应该用 -> None 进行注解。原因比较微妙。如果假定__init__ 缺省用 -> None 作为返回类型注解,那么是否意味着无参数、不带注解的__init__ 方法还需要做类型检查?与其任其模棱两可或引入异常,还不如规定 __init__ 应该带有返回类型注解,默认表现与其他方法相同。
类型检查程序应该对函数主体和所给注解的一致性进行检查。这些注解还可以用于检查其他被检函数对该函数的调用是否正确。
类型检查程序应该尽力推断出尽可能多的信息。最低要求是能够处理内置装饰器 @ property、@ staticmethod 和 @classmethod。
类型定义的语法(Type definition syntax)
这里的语法充分利用了 PEP 3107 风格的注解,并加入了以下章节介绍的一些扩展。类型提示的基本格式就是,把类名填入函数注解的位置:
def greeting(name: str) -> str:
return 'Hello ' + name
以上表示参数 name 的预期类型为 str。类似地,预期的函数返回类型为 str。
其类型是特定参数类型的子类型的表达式也被该参数接受。
可接受的类型提示(Acceptable type hints)
类型提示可以是内置类(含标准库或第三方扩展模块中定义的)、抽象基类、types 模块中提供的类型和用户自定义类(含标准库或第三方库中定义的)。
虽然注解通常是类型提示的最佳格式,但有时更适合用特殊注释(comment)或在单独发布的存根文件中表示。示例参见下文。
注解必须是有效的表达式,其求值过程不会让定义函数时引发异常,不过向前引用(forward reference)的用法还请参见下文。
注解应尽量保持简单,否则静态分析工具可能无法对其进行解析。例如,动态计算出来的类型就不大能被理解。本项要求是有意含糊其辞的,根据讨论结果可以在本 PEP 的未来版本中加入某些包含和排除项。
此外,以下结构也是可以用作类型注解的:None、Any、Union、Tuple、Callable、用于构建由 typing 导出的类(如 Sequence 和 Dict)的所有抽象基类(ABC)及其替代物(stand-in)、类型变量和类型别名。
以下章节介绍的特性当中,所有用于提供支持的新引入类型名(例如 Any 和 Union)都在 typing 模块中给出了。
None 的用法(Using None)
当 None 用于类型提示中时,表达式 None 视作与 type(None) 等价。
类型别名(Type aliases)
定义类型别名很简单,只要用变量赋值语句即可:
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
如果软件框架需要返回特定签名的回调函数,则可以采用 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
在声明返回 Callable 类型时也可以不指定调用签名,只要用省略号(3个句点)代替参数列表即可:
def partial(func: Callable[..., str], *args) -> Callable[..., str]:
# Body
请注意,省略号两侧并不带方括号。在这种情况下,回调函数的参数完全没有限制,并且照样可以使用带关键字(keyword)的参数。
因为带关键字参数的回调函数并不常用,所以当前不支持指定 Callable 类型的带关键字参数。同理,也不支持参数数量可变的回调函数签名。
因为 type.Callable 带有双重职能,用于替代 collections.abc.Callable,所以 isinstance(x, typing.Callable) 的实现与 isinstance(x, collections.abc.Callable) 兼容。但是,isinstance(x, typing.Callable[...]) 是不受支持的。
泛型(Generics)
因为容器中的对象类型信息无法以通用的方式做出静态推断,所以抽象基类已扩展为支持预约(subscription)特性,以标明容器内元素的预期类型。例如:
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() 的参数必须是一个字符串,该字符等于分配给它的变量名。类型变量不允许重定义(redefine)。
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。
用户自定义的泛型类型(User-defined generic types)
把 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] 定义了带有1个类型参数 T 的 LoggedVar 类。这也使得 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]
...
泛型元类不受支持。
类型变量的作用域规则(Scoping rules for type variables)
类型变量遵循常规的名称解析规则。但在静态类型检查的上下文中,存在一些特殊情况:
- 泛型函数中用到的类型变量可以被推断出来,以便在同一代码块中表示不同的类型。例如:
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
实例化通用类和类型清除(Instantiating generic classes and type erasure)
当然可以对用户自定义的泛型类进行实例化。假定编写了以下继承自 Generic[T] 的 Node 类:
from typing import TypeVar, Generic
T = TypeVar('T')
class Node(Generic[T]):
...
若要创建 Node 的实例,像普通类一样调用 Node() 即可。在运行时,实例的类型(类)将会是 Node。但是对于类型检查程序而言,会要求具备什么类型呢?答案取决于调用时给出多少信hu息。如果构造函数(__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] 是可相互区别的类对象,但通过实例化创建对象的运行时类不会记录该区别。这种行为被称作“类型清除(type erasure)”。在 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] 这种带下标的类会有一定的运行开销。其次,使用类型别名的可读性会更好。
用任意泛型类型作为基类(Arbitrary generic types as base classes)
Generic[T] 只能用作基类,它可不合适当作类型来使用。不过上述示例中的用户自定义泛型类型(如 LinkedList[T]),以及内置的泛型类型和抽象基类(如 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。
抽象泛型类型(Abstract generic types)
Generic 使用的元类是 abc.ABCMeta 的一个子类。通过包含抽象方法或属性,泛型类可以成为抽象基类,并且泛型类也可以将抽象基类作为基类而不会出现元类冲突。
带类型上界的类型变量(Type variables with an upper bound)
类型变量可以用 bound=<type> 指定类型上界(注意 <type> 本身不能由类型变量参数化)。这意味着,替换(显式或隐式)类型变量的实际类型必须是上界类型的子类型。常见例子就是定义一个 Comparable 类型,这样就足以捕获最常见的错误了:
from typing import TypeVar
class Comparable(metaclass=ABCMeta):
@abstractmethod
def __lt__(self, other: Any) -> bool: ...
... # __gt__ etc. as well
CT = TypeVar('CT', bound=Comparable)
def min(x: CT, y: CT) -> CT:
if x < y:
return x
else:
return y
min(1, 2) # ok, return type int
min('x', 'y') # ok, return type str
请注意,以上代码还不够理想,比如 min('x', 1) 在运行时是非法的,但类型检查程序只会推断出返回类型是 Comparable。不幸的是,解决这个问题需要引入一个强大且复杂得多的概念,F有界多态性(F-bounded polymorphism)。后续可能还会再来讨论这个问题。
类型上界不能与类型约束一起使用(如 AnyStr 中的用法,参见之前的示例),类型约束会使得推断出的类型一定是约束类型之一,而类型上界则只要求实际类型是上界类型的子类型。
协变和逆变(Covariance and contravariance)
不妨假定有一个 Employee 类及其子类 Manager。假如有一个函数,参数用 List[Employee] 做了注解。那么调用函数时是否该允许使用类型为 List[Manager] 的变量作参数呢?很多人都会不计后果地回答“是的,当然”。但是除非对该函数了解更多信息,否则类型检查程序应该拒绝此类调用:该函数可能会在 List 中加入 Employee 类型的实例,而这将与调用方的变量类型不符。
事实证明,以上这种参数是有逆变性的,直观的回答(如果函数不对参数作出修改则没问题!)是要求这种参数具备协变性。有关这些概念的详细介绍,请参见 Wikipedia 和 PEP 483。这里仅演示一下如何对类型检查程序的行为进行控制。
默认情况下,所有泛型类型的变量均被视作不可变的,这意味着带有 List[Employee] 这种类型注解的变量值必须与类型注解完全相符,不能是类型参数的子类或超类(上述示例中即为Employee)。
为了便于声明可接受协变或逆变类型检查的容器类型,类型变量可带有关键字参数 covariant=True 或 convariant=True。两者只能有一个。如果泛型类型带有此类变量定义,则其变量会被相应视为具备协变或逆变性。按照约定,建议对带有 covariant=True 定义的类型变量命名时采用 _co 结尾,而对于带有 convariant=True 定义的类型变量则以 _contra 结尾来命名。
以下典型示例将会定义一个不可修改(immutable)或只读的容器类:
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)则声明为不可变的(invariant)。协变类型的一个例子是 Generator 类型,其 send() 的参数类型是可协变的(参见下文)。
注意:协变和逆变并不是类型变量的特性,而是用该变量定义的泛型类的特性。可变性仅适用于泛型类型,泛型函数则没有此特性。泛型函数只允许采用不带 covariant 和 convariant 关键字参数的类型变量进行定义。例如以下示例就很不错:
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
...
数值类型的继承关系(The numeric tower)
PEP 3141 定义了 Python 的数值类型层级关系(numeric tower),并且 stdlib 的模块 numbers 实现了对应的抽象基类(Number、Complex、Real、Rational 和 Integral)。关于这些抽象基类是存在一些争议,但内置的具体实现的数值类 complex、float 和 int 已得以广泛应用(尤其是后两个类:-)。
本 PEP 提出了一种简单、快捷、几乎也是高效的方案,用户不必先写 import numbers 语句再使用 umbers.Float:只要注解为 float 类型,即可接受 int 类型的参数。类似地,注解为 complex 类型的参数,则可接受 float 或 int 类型。这种方案无法应对实现抽象基类或 Fractions.Fraction 类的类,但可以相信那些用户场景极为罕见。
向前引用(Forward references)
当类型提示包含尚未定义的名称时,未定义名称可以先表示为字符串字面量(literal),稍后再作解析。
在定义容器类时,通常就会发生这种情况,这时在某些方法的签名中会出现将要定义的类。例如,以下代码(简单的二叉树实现的开始部分)将无法生效:
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 存在循环导入(circular import)处理机制,往往不可能直接导入所有要用到的模型:
# 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,因为在 a 定义类 A 之前就打算从 model/a.py 导入它。解决办法是换成只导入模块,并通过_module_._class_名引用 models:
# 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 类型(Union types)
因为一个参数可接受数量有限的几种预期类型是常见需求,所以系统新提供了一个特殊的工厂类,名为 Union。例如:
from typing import Union
def handle_employees(e: Union[Employee, Sequence[Employee]]) -> None:
if isinstance(e, Employee):
e = [e]
...
Union[T1, T2, ...] 生成(factor)的类型是所有 T、T2 等类型的超级类型(supertype),因此只要是这些类型之一的值就可被 Union[T1, T2, ...] 注解的参数所接受。
Union 类型的一种常见情况是 Optional 类型。除非函数定义中提供了默认值 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 时假定采用 Optional 类型,如下所示:
def handle_employee(e: Employee = None): ...
将被视为等效于:
def handle_employee(e: Optional[Employee] = None) -> None: ...
现在不再推荐这种做法了。类型检查程序应该与时俱进,将需要 Optional 类型的地方明确指出来。
用 Union 实现单实例类型的支持(Support for singleton types in unions)
单实例通常用于标记某些特殊条件,特别是 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 类型(The Any type)
Any 是一种特殊的类型。每种类型都与 Any 相符。可以将其视为包含所有值和所有方法的类型。请注意,Any 和内置的类型对象完全不同。
当某个值的类型为 object 时,类型检查程序将拒绝几乎所有对其进行的操作,将其赋给类型更具体的变量(或将其用作返回值)将是一种类型错误。反之,当值的类型为Any时,类型检查程序将允许对其执行的所有操作,并且
Any 类型的值可以赋给类型更具体(constrained)的变量(或用作返回值)。
不带类型注解的函数参数假定就是用 Any 作为注解的。如果用了泛型类型但又未指定类型参数,则也假定参数类型为 Any:
from typing import Mapping
def use_map(m: Mapping) -> None: # Same as Mapping[Any, Any]
...
上述规则也适用于 Tuple,在类型注解的上下文中,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 类型(The NoReturn type)
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]:
...
类对象的类型(The type of class objects)
有时会涉及到类对象,特别是从某个类继承而来的类对象。类对象可被写为 Type[C],这里的 C 是一个类。为了清楚起见,C 在用作类型注解时指的是类 C 的实例,Type[C] 指的是 C 的子类。这类似于对象和类型之间的区别。
例如,假设有以下类:
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]) 之类的调用将被类型检查程序拒绝(并且会运行失败,因为 union 无法实例化)。
请注意,用类的 union 作 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[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
...
为实例和类方法加类型注解(Annotating instance and class methods)
大多数情况下,类和实例方法的第一个参数不需要加类型注解,对实例方法而言假定它的类型就是所在类(的类型),对类方法而言它则是所在类对象对应的类型对象(的类型)。另外,实例方法的第一个参数加类型注解时可以带有一个类型变量。这时返回类型可以采用相同的类型变量,从而使该方法成为泛型函数。例如:
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" 这种晦涩语句。
运行时检查还是类型检查?(Runtime or type checking?)
有时候,有些代码必须由类型检查程序(或其他静态分析工具)进行检查,而不应拿去运行。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 注释中无需加引号。
这种做法对于处理循环导入也会比较有用。
可变参数列表和默认参数值(Arbitrary argument lists and default argument values)
可变参数列表也可以加注类型注解,以下定义是可行的:
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]。
在存根(stub)文件中,将参数声明为带有默认值,但不指定实际的默认值,这会很有用。例如:
def foo(x: AnyStr, y: AnyStr = ...) -> AnyStr: ...
默认值应该是如何的?""、b"" 或 None 都不符合类型约束。
这时可将默认值指定为省略号,其实就是以上示例。
只采用位置参数(Positional-only arguments)
有一些函数被设计成只能按位置接收参数,并希望调用者不要使用参数名称,不通过关键字给出参数。名称以__开头的参数均被假定为只按位置访问,除非同时以__结尾:
def quux(__x: int, __y__: int = 0) -> None: ...
quux(3, __y__=1) # This call is fine.
quux(__x=3) # This call is an error.
为生成器函数和协程加类型注解(Annotating generator functions and coroutines)
生成器函数的返回类型可以用 type.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 中引入的协程(coroutine)可用与普通函数相同的语法进行类型注解。但是,返回类型的类型注解对应的是 await 表达式的类型,而不是协程的类型:
async def spam(ignored: int) -> str:
return 'spam'
async def foo() -> None:
bar = await spam(42) # type: str
type.py 模块提供了一个抽象基类 collections.abc.Coroutine 的泛型版本,以支持可异步调用(awaitable)特性,同时支持 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
该模块还为无法指定更精确类型的情况提供了泛型抽象基类 Awaitable、AsyncIterable 和 AsyncIterator:
def op() -> typing.Awaitable[str]:
if cond:
return spam(42)
else:
return asyncio.Future(...)
与函数注解其他用法的兼容性(Compatibility with other uses of function annotations)
有一些函数注解的使用场景,与类型提示是不兼容的。这些用法可能会引起静态类型检查程序的混乱。但因为类型提示的注解在运行时不起作用(计算注解表达式、将注解存储在函数对象的 __annotations__ 属性中除外),所以不会让程序报错,只是可能会让类型检查程序发出虚报警告或错误。
如果要让某部分程序不受类型提示的影响,可以用以下一种或几种方法进行标记:
- 用
# type: ignore 加注释(comment);
- 为类或函数加上
@no_type_check 装饰符(decorator);
- 为自定义类或函数装饰符加上
@no_type_check_decorator 标记。
更多详情,请参见后续章节。
为了最大程度与脱机类型检查过程保持兼容,将依赖于类型注解的接口改成其他机制(例如装饰器)可能比较合适些。不过这在 Python 3.5 中没什么关系。更多讨论请参见后续的“未被采纳的其他方案”。
来源:https://www.cnblogs.com/popapa/p/PEP484.html |