本 PEP 列出了 PEP 484 的理论知识。
本文档列出了 Python 3.5 新引入的类型提示提案的理论知识。因为尚有很多细节需要制定,所以这还算不上是一个完整的提案或规范,但没有这里罗列的理论,就很难对更详细的规范进行讨论。我们首先回顾类型理论的基本概念,再解释渐进定型(gradual typing),然后再陈述一些通用规则并定义可以在注释中使用的的新的特殊类型(如 Union
);最后我们定义了实现泛型类型的方法和类型提示的实用方面。
t1、
t2及
u1、
u2等表示类型。有时使用
ti或
tj表示
t1、
t2` 等中的任意一个。
T
, U
等表示类型变量(由 TypeVar()
定义,参见下文)。
对象、类由 class
语句定义,实例用标准的 PEP 8 约定进行表示。
在此PEP上下文中应用于类型的符号 ==
表示两个表达式表示同一类型。
请注意, PEP 484 在类型和类之间进行了区分(类型是类型检查器的概念,而类是运行时概念)。在此PEP中,我们澄清了这种区别,但避免了不必要的严格性,以便在类型检查器的实现中提供更大的灵活性。
在文献中,类型的概念有很多定义。在这里,我们假设类型是一组值和一组可以应用于这些值的函数。
有几种定义特定类型的方法:
显式列出所有值。比如 True
和 False
形成了 bool
类型。
通过指定可与类型变量一起使用的函数。例如。具有 __len__
方法的所有对象均形成 Sized
类型。 [1、2、3]
和 'abc'
都属于此类型,因为可以在它们上调用 len
函数:
len([1, 2, 3]) # OK
len('abc') # also OK
len(42) # not a member of Sized
通过简单定义一个类,例如,定义如下的一个类:
class UserID(int):
pass
还有更复杂的类型。例如,可以将类型 FancyList
定义为仅包含 int
、str
或其子类实例的所有列表。值[1,'abc',UserID(42)]
具有此类型。
对于用户而言,重要的是能够以类型检查器可以理解的形式定义类型。该PEP的目标是提出一种使用 PEP 3107 语法为变量和函数的类型注释定义类型的系统方法。这些注释可用于避免许多错误、用作文档,甚至可用于提高程序执行的速度。在这里,我们仅专注于通过使用静态类型检查器来避免错误。
静态类型检查器的关键概念是子类型关系。它来自于以下问题:如果 first_var
的类型为 first_type
,second_var
的类型为 second_type
,那么赋值语句 first_var = second_var
是否安全呢?
关于何时 应该 是安全的的一个严格标准是:
second_type
中的每个值也位于 first_type
的值集中;并且first_type
中的每个函数也都在 second_type
的函数集中。定义的这个关系既是子类型关系。
根据以上定义:
一个直观的例子:每个 Dog
都是 Animal
,而 Dog
也具有更多功能,例如可以吠叫,因此,Dog
是 Animal
的子类型。相反,Animal
不是 Dog
的子类型。
一个更正式的例子:整数是实数的子类型。实际上,每个整数当然也是一个实数,并且整数支持更多的运算,例如按位移位 <<
和 >>
:
lucky_number = 3.14 # type: float
lucky_number = 42 # Safe
lucky_number * 2 # This works
lucky_number << 5 # Fails
unlucky_number = 13 # type: int
unlucky_number << 5 # This works
unlucky_number = 2.72 # Unsafe
让我们再考虑一个棘手的示例:如果 List[int]
表示由仅包含整数的所有列表构成的类型,则它 不是 List[float]
的子类型,List[float]
是由只包含实数的所有列表组成的。子类型化的第一个条件成立,但将一个实数附加到列表末尾则仅适用于 List[float]
,因此第二个条件失败:
def append_pi(lst: List[float]) -> None:
lst += [3.14]
my_list = [1, 3, 5] # type: List[int]
append_pi(my_list) # Naively, this should be safe...
my_list[-1] << 5 # ... but this fails
有两种广泛的方法可以向类型检查器声明子类型信息。
在名义子类型化(nominal subtyping)中,类型树基于类树,即 UserID
被视为 int
的子类型。此方法应在类型检查器的控制下使用,因为在Python中,这种方法可以以不兼容的方式覆盖属性:
class Base:
answer = '42' # type: str
class Derived(Base):
answer = 5 # should be marked as error by type checker
在结构子类型化(structural subtyping)中,子类型关系是从声明的方法中推导出来的,即 UserID
和 int
将被视为同一类型。尽管这有时可能会引起混乱,但结构子类型被认为更灵活。我们努力为这两种方法提供支持,以便除了名义子类型化之外还可以使用结构信息。
渐进类型(gradual typing)允许仅注释程序的一部分,因此可以利用动态和静态类型的理想方面。
我们定义一个新的关系 is-consistent-with
,它与 is-subtype-of
类似,不同之处在于当涉及到新的 Any
类型时它不是可传递的。 (两个关系都不对称。)如果 a_value
的类型与 a_variable
的类型一致,则将 a_value
分配给 a_variable
是可以的。 (将其与表明OO编程的基本原理之一的 “如果 a_value
的类型是 a_variable
的类型的子类型” 进行比较)is-consistent-with
关系由三个规则定义:
t1
是 t2
的子类型,则类型 t1
与 类型t2
一致。(但反向则不成立。)Any
与每种类型都一致。(但 Any
不是任意类型的子类型。)Any
一致。(但每种类型都不是 Any
的子类型)就这样!有关更长的解释和动机,请参见 Jeremy Siek 的博客文章 “什么是渐进类型”。Any
类型可以视为具有所有值和所有方法的类型。结合上面的子类型定义,这将 Any
局部地放在类型层次结构的顶部(具有所有值)和底部(具有所有方法)。将此与 object
进行对比——与大多数类型不一致(例如,您不能在需要 int
的地方使用object()
实例)。 换句话说, Any
和 object
在用于注解参数时均表示 “允许任何类型”,但无论期望使用哪种类型,都只能传递 Any
(本质上,Any
声明回退到动态类型并关闭来自静态检查器的抱怨)。
这是显示这些规则在实践中如何工作的示例:
假定有 Employee
类及其子类 Manager
:
class Employee: ...
class Manager(Employee): ...
假定变量 worker
声明为 Employee
类型:
worker = Employee() # type: Employee
此后将 Manager
实例赋值给 worker
是可以的(规则1):
worker = Manager()
但是将 Employee
赋值给声明为 Manager
类型的变量则是不行的:
boss = Manager() # type: Manager
boss = Employee() # Fails static check
然而,假设有一个类型为 Any
的变量:
something = some_func() # type: Any
则将 something
赋值给 worker
是没问题的(规则2):
worker = something # OK
当然将 worker
赋值给 something
也没问题(规则3),但这不需要用到一致性的概念。
something = worker # OK
在Python中,类是由 class
语句定义的对象工厂,也可以由 type(obj)
内置函数返回。类是一个动态的运行时概念。
类型概念已在上面进行了描述,类型出现在变量和函数类型注解中,可以从下面描述的构造块中构造出来,并由静态类型检查器使用。
每个类都是上述类型。但是实现一个类来精确表示给定类型的语义是棘手且容易出错的,这不是 PEP 484 的目标。不应将 PEP 484 中描述的静态类型与运行时类混淆。例如:
int
是类,同时也是类型。
UserID
是类,同时也是类型。
Union[str, int]
是类型,但不是一个合适的类:
class MyUnion(Union[str, int]): ... # raises TypeError
Union[str, int]() # raises TypeError
类型接口是通过类实现的,即在运行时可以计算如 Generic[T].__bases__
这样的对象。但是要强调类和类型之间的区别,请遵循以下一般规则:
Any
,Union
等),尝试这样做将引发 TypeError
。 (但可以是 Generic
的非抽象子类。)Generic
和从其派生的类之外,下面定义的任何类型都不能被子类化。isinstance
或 issubclass
中(未参数化的泛型除外),所有这些都会引发TypeError
。Any
任何类型均与 Any
一致,Any
也与任何类型一致(参见上文)。
Union[t1, t2, …]
属于 t1
等中 至少一个 的子类型的类型是该类型的子类型。
t1
等的子类型的 Union
是该类型的子类型。比如 Union[int, str]
就是 Union[int, float, str]
的子类型。Union[int, str] == Union[str, int]
。ti
本身是 Union
,则结果将进行扁平化处理。例如:Union[int, Union[float, str]] == Union[int, float, str]
。ti
和 tj
具有子类型关系,则保留更泛化的类型。例如:Union[Employee, Manager] == Union[Employee]
。Union[t1]
仅返回 t1
。Union[]
和 Union[()]
都是非法的。Union[..., object, ...]
将返回 object
。Optional[t1]
Union[t1, None]
的别名,例如 Union[t1, type(None)]
。
Tuple[t1, t2, …, tn]
由 t1
等类型的实例构成的元组。比如 Tuple[int, float]
表示包含两个项的元组,第一项是 int
类型,第二项是 float
类型,例如 (42, 3.14)
。
Tuple[u1, u2, ..., um]
和 Tuple[t1, t2, ..., tn]
具有相同的长度,即 n == m
,并且对于每一个 ui
和 ti
,前者都是后者的子类型,那么 Tuple[u1, u2, ..., um]
是 Tuple[t1, t2, ..., tn]
的子类型。Tuple[()]
表示。Tuple[t1, ...]
。 (其中的三个点是 Python 中的 ellipse
类的单例对象)Callable[[t1, t2, …, tn], tr]
位置参数类型为 t1
等、返回类型为 tr
的函数。参数列表可以为空,即 n == 0
。无法指示可选参数或关键字参数,也无法指示 varargs
,但是可以通过 Callable[..., tr]
这样的形式来表示完全不检查参数列表。
也许还会加入如下一些类型:
t1
等 每一个 的子类型的类型是该类型的子类型。 (与 Union
相比,Union
在其定义中使用了 至少一个 ,而 Intersection
使用了 每一个)
Intersection
将被展开,例如, Intersection[int, Intersection[float, str]] == Intersection[int, float, str]
。Intersection
是包含的类型较多的 Intersection
的超类型。比如, Intersection[int, str]
是 Intersection[int, float, str]
的超类型。Intersection
就是参数本身的类型。比如 Intersection[int]
就是 int
。Intersection[str, Employee, Manager]
就是 Intersection[str, Manager]
。Intersection[]
和 Intersection[()]
都是非法的。Any
将会消失,例如 Intersection[int, str, Any] == Intersection[int, str]
。Intersection[Any, object]
就是 object
。Intersection
和 Union
的关系是比较复杂的,但如果理解了普通集合的交集和并集,就比较容易了(注意类型的集合的大小可以是无限的,因为新建的子类的数量是没有限制的。)上面定义的基本构建块允许以通用方式构造新类型。例如,Tuple
可以使用具体类型 float
创建一个新的具体类型 Vector = Tuple[float, ...]
,或者可以使用另一种类型 UserID
创建另一种新的具体类型 Registry = Tuple[UserID, ...]
。这种语义称为泛型类型构造器,它类似于函数的语义,但是函数接受值并返回值,而泛型类型构造器接受类型并"返回"类型。
一个特定的类或一个函数以这种类型通用的方式运行是很常见的。考虑两个例子:
容器类,比如 list
或 dict
,典型情况是只包含一种类型的数据。因此用户可能想使用如下的方式进行注解:
class MyUnion(Union[str, int]): ... # raises TypeError
Union[str, int]() # raises TypeError
以下函数可能带两个 int
类型的参数并返回一个 int
类型的值,也可能带两个 float
类型的参数并返回一个 float
类型的值,等等:
def add(x, y):
return x + y
add(1, 2) == 3
add('1', '2') == '12'
add(2.7, 3.5) == 6.2
为了能够在第一个示例所示的情况下进行类型注解,内置容器和容器抽象基类使用类型参数进行了扩展,以便它们充当泛型类型构造器。充当泛型类型构造器的类称为 泛型。例如:
from typing import Iterable
class Task:
...
def work(todo_list: Iterable[Task]) -> None:
...
此处的 Iterable
就是一个泛型类型,它采用具体类型 Task
,并返回具体类型 Iterable[Task]
。
以类型通用的方式运行的函数(如第二个示例所示)称为 泛型函数。泛型函数的类型注解允许使用类型变量 。带有遵循泛型类型的类型变量的语义与函数中参数的语义有些相似。但是没有为类型变量分配具体的类型,静态类型检查器的任务是找到它们的可能值,并在找不到时警告用户。例如:
def take_first(seq: Sequence[T]) -> T: # a generic function
return seq[0]
accumulator = 0 # type: int
accumulator += take_first([1, 2, 3]) # Safe, T deduced to be int
accumulator += take_first((2.7, 3.5)) # Unsafe
类型变量广泛用于类型注解中,类型检查器中类型推断的内部机制通常也建立在类型变量之上。因此,让我们详细介绍一下它们。
X = TypeVar('X')
声明一个唯一的类型变量。名称必须与变量名称匹配。默认情况下,类型变量覆盖所有可能的类型。例:
def do_nothing(one_arg: T, other_arg: T) -> None:
pass
do_nothing(1, 2) # OK, T is int
do_nothing('abc', UserID(42)) # also OK, T is object
Y = TypeVar('Y', t1, t2, ...)
同上,约束为 t1
等类型。其行为与Union[t1, t2, ...]
类似。受约束的类型变量仅在约束 t1
等范围内精确地变化;约束的子类被 t1
等中最底层(most-derived)基类替换。示例:
带有受限类型变量的函数类型注解:
S = TypeVar('S', str, bytes)
def longest(first: S, second: S) -> S:
return first if len(first) >= len(second) else second
result = longest('a', 'abc') # The inferred type for result is str
result = longest('a', b'abc') # Fails static type check
上述例子中,longest()
两个参数的类型必须相同(str
或 bytes
),而且即便参数是 str
的子类,其返回类型依然会是 str
而非那个子类(参见以下例子)。
以下例子作为对照,如果类型变量是非受限的,将会把子类作为返回类型,比如:
S = TypeVar('S')
def longest(first: S, second: S) -> S:
return first if len(first) >= len(second) else second
class MyStr(str): ...
result = longest(MyStr('a'), MyStr('abc'))
result
的推断类型为 MyStr
(而在 AnyStr
示例中为 str
)。
以下仍然作为对照,如果用了 Union
,则返回类型必须为 Union
:
U = Union[str, bytes]
def longest(first: U, second: U) -> U:
return first if len(first) >= len(second) else second
result = longest('a', 'abc')
即使两个参数均为 str
,上述 result
的推断类型仍为 Union[str, bytes]
。
请注意,类型检查器将拒绝此函数:
def concat(first: U, second: U) -> U:
return x + y # Error: can't concatenate str and bytes
对于此类参数只能同时更改其类型的情况,应使用受约束的类型变量。
用户可以使用特殊的构造块 Generic
将其类声明为泛型类型。定义 class MyGeneric(Generic[x, y, ...]): ...
定义了类型变量 X
等的泛型类型 MyGeneric
。MyGeneric
本身成为可参数化的,例如, MyGeneric[int, str, ...]
是一种使用 X -> int
等替换的特殊类型。例如:
class CustomQueue(Generic[T]):
def put(self, task: T) -> None:
...
def get(self) -> T:
...
def communicate(queue: CustomQueue[str]) -> Optional[str]:
...
由泛型类型派生的类就成了泛型。一个类可以子类化多个泛型类型。但是泛型返回的某具体类型的派生类则不是泛型。例如:
class TodoList(Iterable[T], Container[T]):
def check(self, item: T) -> None:
...
def check_all(todo: TodoList[T]) -> None: # TodoList is generic
...
class URLList(Iterable[bytes]):
def scrape_all(self) -> None:
...
def search(urls: URLList) -> Optional[bytes] # URLList is not generic
...
泛型的子类化将会强制为对应的类型加上子类型关系,因此在上述例子中, TodoList[t1]
是 Iterable[t1]
的子类型。
泛型类型可以通过几个步骤进行专门化(索引化)。每个类型变量都可以由特定类型或其他泛型类型替换。如果 Generic
出现在基类列表中,则它应包含所有类型变量,并且类型参数的顺序由它们在 Generic
中出现的顺序决定。例子:
Table = Dict[int, T] # Table is generic
Messages = Table[bytes] # Same as Dict[int, bytes]
class BaseGeneric(Generic[T, S]):
...
class DerivedGeneric(BaseGeneric[int, T]): # DerivedGeneric has one parameter
...
SpecificType = DerivedGeneric[int] # OK
class MyDictView(Generic[S, T, U], Iterable[Tuple[U, T]]):
...
Example = MyDictView[list, int, str] # S -> list, T -> int, U -> str
如果泛型出现在类型注解中,并且省略了类型变量,则假定该类型为 Any
。这种形式可以用作动态类型的后备,并且可以与 issubclass
和 isinstance
一起使用。实例中的所有类型信息都会在运行时删除。例如:
def count(seq: Sequence) -> int: # Same as Sequence[Any]
...
class FrameworkBase(Generic[S, T]):
...
class UserClass:
...
issubclass(UserClass, FrameworkBase) # This is OK
class Node(Generic[T]):
...
IntNode = Node[int]
my_node = IntNode() # at runtime my_node.__class__ is Node
# inferred static type of my_node is Node[int]
假设 t2
是 t1
的子类型,那么泛型类型的构造器 GenType
被称为:
t1
和 t2
,GenType[t2]
是 GenType[t1]
的子类型。t1
和 t2
,GenType[t1]
是 GenType[t2]
的子类型。为了更好地理解以上定义,让我们用普通函数来进行分析。假定有以下函数:
def cov(x: float) -> float:
return 2*x
def contra(x: float) -> float:
return -x
def inv(x: float) -> float:
return x*x
如果 x1 < x2
,则 总是 有 cov(x1) < cov(x2)
和 contra(x2) < contra(x1)
,但对于 inv
则无法确定。将 <
替换为 “是子类型”(is-subtype-of),将函数替换为泛型构造器,就得到了协变、逆变和不变行为的例子。下面看几个实例:
Union
会对所有参数表现为协变。实际上,如上所述,如果 t1
等是 u1
等类型的子类型,则 Union[t1, t2, ...]
就是 Union[u1, u2, ...]
的子类型。FrozenSet[T]
也是协变的。不妨将 T
替换为 int
和 float
。首先 int
是 float
的子类型;其次 FrozenSet[int]
的值集显然是 FrozenSet[float]
的值的子集,而 FrozenSet[float]
的函数集则是FrozenSet[int]
的函数集的子集。因此,根据定义,FrozenSet[int]
是 FrozenSet[float]
的子类型。List[T]
是不变的。实际上,尽管 List[int]
的值集是 List[float]
的值的子集,但只有 int
能够附加到 List[int]
中去。因此, List[int]
不是 List[float]
的子类型。这是可变类型的典型情况,它们通常是不变的。可调用类型是说明(有点违反直觉)逆变行为的最佳示例之一。它在返回类型上是协变的,但参数类型则是逆变的。对于仅仅是返回类型不同的两个可调用类型,可调用类型的子类型关系遵循返回类型的子类型关系。例如:
Callable[[], int]
是 Callable[[], float]
的子类型。Callable[[], Manager]
是 Callable[[], Employee]
的子类型。而对于仅一个参数类型不同的两个可调用类型,则其子类型关系与参数类型的子类型关系 方向相反。例如:
Callable[[float], None]
是 Callable[[int], None]
的子类型。Callable[[Employee], None]
是 Callable[[Manager], None]
的子类型。是的,你没有看错。确实,如果期望计算经理薪资的函数为:
def calculate_all(lst: List[Manager], salary: Callable[[Manager], Decimal]):
...
那么可以为任何雇员计算薪水的 Callable[[Employee], Decimal]
也是可以接受的。
带 Callable
的示例说明了如何为函数进行更精确的类型注解:为每个参数选择最通用的类型,为返回值选择最特定的类型。
通过使用特殊的关键字 covariant
和 contravariant
来定义作为参数的类型变量,可以声明用户定义的泛型类型的协变性和逆变性。默认情况下,类型是不变的。例如:
T = TypeVar('T')
T_co = TypeVar('T_co', covariant=True)
T_contra = TypeVar('T_contra', contravariant=True)
class LinkedList(Generic[T]): # invariant by default
...
def append(self, element: T) -> None:
...
class Box(Generic[T_co]): # this type is declared covariant
def __init__(self, content: T_co) -> None:
self._content = content
def get_content(self) -> T_co:
return self._content
class Sink(Generic[T_contra]): # this type is declared contravariant
def send_to_nowhere(self, data: T_contra) -> None:
with open(os.devnull, 'w') as devnull:
print(data, file=devnull)
注意,尽管协变性和逆变性是通过类型变量定义的,但它不是类型变量的属性,而是泛型类型的属性。在派生泛型的复杂定义中,协变性和逆变性仅由使用的类型变量确定。下面是一个复杂的例子:
T_co = TypeVar('T_co', Employee, Manager, covariant=True)
T_contra = TypeVar('T_contra', Employee, Manager, contravariant=True)
class Base(Generic[T_contra]):
...
class Derived(Base[T_co]):
...
类型检查器从第二个声明中发现 Derived[Manager]
是 Derived[Employee]
的子类型,而 Derived[t1]
是Base[t1]
的子类型。如果我们用 <
表示 “is-subtype-of” 关系,那么这种情况下的子类型的完整图将是:
Base[Manager] > Base[Employee]
v v
Derived[Manager] < Derived[Employee]
因此类型检查程序也将发现这些关系,比如 Derived[Manager]
就是 Base[Employee]
的子类型。
关于类型变量、泛型、协变性、逆变性的更多信息,请参阅 PEP 484、mypy 中有关泛型的文档 和 Wikipedia。
有些事情与理论无关,但使实际使用更方便。 (这不是完整的列表;我可能错过了一些,有些仍然有争议或未完全指定。)
None
可以代替 type(None)
。例如: Union[t1, None] == Union[t1, type(None)]
。Point = Tuple[float, float]
def distance(point: Point) -> float: ...
class MyComparable:
def compare(self, other: 'MyComparable') -> int: ...
T = TypeVar('T', bound=complex)
def add(x: T, y: T) -> T:
return x + y
T_co = TypeVar('T_co', covariant=True)
class ImmutableList(Generic[T_co]): ...
lst = [] # type: Sequence[int]
cast(T, obj)
进行转换,例如:zork = cast(Any, frobozz())
stub
模块,请参见 PEP 484 。(也可查看 typing.py 模块)
collections.abc
中的所有类型(但 Set
重命名为 AbstractSet
)。Dict
、List
、Set
、FrozenSet
等。re.Pattern[AnyStr]
、re.Match[AnyStr]
。io.IO[AnyStr]
、io.TextIO ~ io.IO[str]
、io.BinaryIO ~ io.IO[bytes]
。