首页 > Python基础教程 >
-
PEP 484 类型提示 -- Python官方文档译文 [原创](2)
若要创建 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]