PEP 483 – 类型提示理论
- 作者:
- Guido van Rossum <guido at python.org>, Ivan Levkivskyi <levkivskyi at gmail.com>
- 讨论至:
- Python-Ideas 邮件列表
- 状态:
- 最终版
- 类型:
- 信息性
- 主题:
- 类型标注
- 创建日期:
- 2014年12月19日
- 发布历史:
摘要
本 PEP 阐述了 PEP 484 中引用的理论。
引言
本文阐述了 Python 3.5 新类型提示提案的理论。它并非一个完整的提案或规范,因为许多细节仍需完善,但它阐述了理论基础,没有这些理论就难以讨论更详细的规范。我们首先回顾类型理论的基本概念;然后解释渐进式类型化;接着阐述一些通用规则并定义可用于注解的新特殊类型(如 Union);最后定义泛型类型的方法和类型提示的实用方面。
符号约定
背景
文献中对类型概念有多种定义。这里我们假设类型是一组值和一组可以应用于这些值的函数。
定义特定类型有几种方法
- 通过显式列出所有值。例如,
True和False构成bool类型。 - 通过指定可与特定类型变量一起使用的函数。例如,所有具有
__len__方法的对象构成Sized类型。[1, 2, 3]和'abc'都属于此类型,因为可以对它们调用lenlen([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],因此第二个条件失败。
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
有两种广泛使用的方法向类型检查器*声明*子类型信息。
在名义子类型化中,类型树基于类树,即 UserID 被认为是 int 的子类型。这种方法应在类型检查器的控制下使用,因为在 Python 中可以以不兼容的方式覆盖属性
class Base:
answer = '42' # type: str
class Derived(Base):
answer = 5 # should be marked as error by type checker
在结构子类型化中,子类型关系是从声明的方法推导出来的,即 UserID 和 int 将被视为相同的类型。虽然这偶尔会引起混淆,但结构子类型化被认为更灵活。我们力求同时支持这两种方法,以便除了名义子类型化之外,还可以使用结构信息。
渐进式类型化摘要
渐进式类型化允许只对程序的一部分进行注解,从而利用动态类型化和静态类型化的优点。
我们定义了一个新的关系,is-consistent-with(一致性),它类似于 is-subtype-of(子类型),不同之处在于当新的类型 Any 参与时,它不具备传递性。(两种关系都不对称。)如果 a_value 的类型与 a_variable 的类型一致,则将 a_value 赋值给 a_variable 是可以的。(与“如果 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]是一种类型,但不是一个 proper 类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一致;它也与所有类型一致(见上文)。 - 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[()]。 - 一个可变长同质元组类型可以写成
Tuple[t1, ...]。(那是三个点,一个字面省略号;是的,那在 Python 语法中是一个有效的标记。)
- 如果
- Callable[[t1, t2, …, tn], tr]。一个函数,其位置参数类型为
t1等,返回类型为tr。参数列表可以为空n==0。无法指示可选参数或关键字参数,也无法指示可变参数,但可以通过写入Callable[..., tr](同样是字面省略号)来表示参数列表完全不进行检查。
我们可以添加
- Intersection[t1, t2, …]。是
t1等中*每个*类型的子类型的类型,都是它的子类型。(与Union进行比较,Union的定义中是*至少一个*而不是*每个*。)- 参数的顺序无关紧要。嵌套的交集被展平,例如
Intersection[int, Intersection[float, str]] == Intersection[int, float, str]。 - 更少类型的交集是更多类型的交集的超类型,例如
Intersection[int, str]是Intersection[int, float, str]的超类型。 - 一个参数的交集就是那个参数,例如
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,通常只包含特定类型的值。因此,用户可能希望像这样对其进行类型注解users = [] # type: List[UserID] users.append(UserID(42)) # OK users.append('Some guy') # Should be rejected by the type checker examples = {} # type: Dict[str, Any] examples['first example'] = object() # OK examples[2] = None # rejected by the type checker
- 以下函数可以接受两个
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 等中最派生的基类替换。示例
- 带有受限类型变量的函数类型注解
AnyStr = TypeVar('AnyStr', str, bytes) def longest(first: AnyStr, second: AnyStr) -> AnyStr: 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,则返回类型也必须是UnionU = 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 first + second # Error: can't concatenate str and bytes
对于参数只能同时改变类型的情况,应使用受限类型变量。
定义和使用泛型类型
用户可以使用特殊构建块 Generic 将其类声明为泛型类型。定义 class MyGeneric(Generic[X, Y, ...]): ... 定义了一个泛型类型 MyGeneric,其类型变量为 X 等。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
- 协变(Covariant),如果对于所有这样的
t1和t2,GenType[t2]是GenType[t1]的子类型。 - 逆变(Contravariant),如果对于所有这样的
t1和t2,GenType[t1]是GenType[t2]的子类型。 - 不变(Invariant),如果以上都不是真的。
为了更好地理解这个定义,让我们类比普通函数。假设我们有
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]也是协变的。让我们用int和float来代替T。首先,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] 的子类型。
实用性
有些事情与理论无关,但使实际使用更方便。(这不是一个完整的列表;我可能遗漏了一些,有些仍然有争议或未完全指定。)
- 在期望类型的地方,
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())
- 其他事项,例如重载和存根模块,请参阅 PEP 484。
typing.py 中预定义的泛型类型和协议
(另请参阅 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]。
版权
本文档根据 Open Publication License 获得许可。
来源:https://github.com/python/peps/blob/main/peps/pep-0483.rst
上次修改时间:2025-02-01 08:59:27 GMT