【PEP483】类型提示的理论
https://peps.python.org/pep-0483/ 2025年5月25日
以下内容全部由KIMI生成。
精简版
PEP 483 为 Python 的类型提示(Type Hints)提供了理论基础,旨在为 Python 3.5 及更高版本引入类型注解功能。以下是稍作扩充的总结:
1. 类型与子类型
- 类型定义:类型是值集合及其上可应用函数的集合,可通过显式值列表、函数声明或类定义来定义。
- 子类型关系:如果一个类型的值集合是另一个类型的子集,且其函数集合是超集,则前者是后者的子类型。例如,
Dog是Animal的子类型,因为每个Dog都是Animal,且Dog有更多功能(如bark)。 - 子类型声明方式:
- 名义子类型:基于类的继承关系。
- 结构子类型:基于方法和属性的声明。
2. 逐步类型化(Gradual Typing)
逐步类型化允许开发者仅对程序的一部分进行类型注解,结合了动态类型和静态类型的优势。它通过“一致”(consistent)关系定义类型兼容性:
- 如果
t1是t2的子类型,则t1与t2一致。 Any与任何类型一致,但不是任何类型的子类型。- 任何类型与
Any一致。
3. 基本类型构建块
- Any:与任何类型一致,常用于需要动态类型检查的场景。
- Union:表示多个类型中的任意一个。例如,
Union[int, str]表示可以是int或str。 - Optional:表示类型可以是
None,例如Optional[int]等同于Union[int, None]。 - Tuple:表示固定长度的元组,例如
Tuple[int, float]表示包含一个int和一个float的元组。 - Callable:表示函数类型,例如
Callable[[int, str], float]表示接受int和str参数并返回float的函数。
4. 泛型类型(Generic Types)
泛型类型允许定义可接受不同类型参数的类或函数:
- 泛型类:如
List[T],表示包含类型为T的元素的列表。 - 泛型函数:通过类型变量(
TypeVar)实现,例如def add(x: T, y: T) -> T,表示函数接受两个相同类型的参数并返回相同类型的值。 - 类型变量:可以通过
TypeVar定义,可以是无约束的(任意类型),也可以是受限的(只能是某些类型之一)。
5. 协变(Covariance)与逆变(Contravariance)
- 协变:如果
t2是t1的子类型,那么GenType[t2]是GenType[t1]的子类型。例如,FrozenSet[int]是FrozenSet[float]的子类型。 - 逆变:如果
t2是t1的子类型,那么GenType[t1]是GenType[t2]的子类型。例如,Callable[[Manager], None]是Callable[[Employee], None]的子类型。 - 不变:既不是协变也不是逆变。例如,
List[T]是不变的,因为List[int]不是List[float]的子类型。
6. 实用特性
- 类型别名:可以为复杂类型定义别名,例如
Point = Tuple[float, float]。 - 前向引用:可以通过字符串引用尚未定义的类,例如
def compare(self, other: 'MyComparable') -> int。 - 类型声明:可以通过注释声明变量类型,例如
lst = [] # type: List[int]。 - 类型转换:使用
cast(T, obj)强制转换类型。
7. 预定义泛型类型和协议
typing 模块中包含了许多预定义的泛型类型,例如 Dict、List、Set 等,以及协议(Protocols)用于定义抽象接口。
PEP 483 提供了 Python 类型提示的理论基础,涵盖了类型、子类型、逐步类型化、泛型类型和协变/逆变等概念,旨在为 Python 提供灵活的类型注解系统,帮助开发者在保持 Python 动态特性的同时,利用静态类型检查避免错误。
PEP 483 是 Python 官方文档中关于类型提示理论的文档,它为 Python 3.5 引入的类型提示功能提供了理论基础。以下是该文档的全文翻译:
PEP 483 – 类型提示的理论
摘要
本文档阐述了 PEP 484 中引用的理论。
引言
本文档阐述了 Python 3.5 中新的类型提示提案的理论。它并非一个完整的提案或规范,因为还有许多细节需要完善,但它提供了理论基础,没有这些理论基础,很难讨论更详细的规范。
符号约定
t1,t2等和u1,u2等是类型。有时我们会用ti或tj来指代“t1,t2等中的任何一个”。T,U等是类型变量(用TypeVar()定义,见下文)。- 对象、用
class语句定义的类以及实例使用标准的 PEP 8 约定表示。 - 在本文档中,符号
==用于类型时,表示两个表达式表示相同的类型。 - 注意,PEP 484 区分了类型和类(类型是类型检查器的概念,而类是运行时的概念)。在本文档中,我们澄清了这种区别,但避免不必要的严格性,以便在类型检查器的实现中提供更多的灵活性。
背景
在文献中,类型的概念有许多定义。在这里,我们假设类型是一组值和一组可以应用于这些值的函数。
有几种方法可以定义特定的类型:
-
通过明确列出所有值。例如,
True和False构成了bool类型。 -
通过指定可以与某类型变量一起使用的函数。例如,所有具有
__len__方法的对象构成了Sized类型。[1, 2, 3]和'abc'都属于这个类型,因为可以对它们调用len:len([1, 2, 3]) # OK len('abc') # 也可以 len(42) # 不是 Sized 的成员 -
通过简单的类定义,例如如果定义了一个类:
那么这个类的所有实例也构成了一个类型。
-
还有一些更复杂的类型。例如,可以定义
FancyList类型为所有只包含int、str或其子类实例的列表。值[1, 'abc', UserID(42)]属于这个类型。
用户能够以类型检查器可以理解的形式定义类型是很重要的。本文档的目标是提出一种系统化的方法,用于定义变量和函数的类型注解,使用 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 # 类型:float
lucky_number = 42 # 安全
lucky_number * 2 # 这可以工作
lucky_number << 5 # 失败
unlucky_number = 13 # 类型:int
unlucky_number << 5 # 这可以工作
unlucky_number = 2.72 # 不安全
让我们再考虑一个棘手的例子:如果 List[int] 表示由仅包含整数的列表构成的类型,那么它不是由仅包含实数的列表构成的 List[float] 的子类型。第一个子类型条件成立,但向列表追加实数只适用于 List[float],因此第二个条件失败:
def append_pi(lst: List[float]) -> None:
lst += [3.14]
my_list = [1, 3, 5] # 类型:List[int]
append_pi(my_list) # 乍一看,这应该是安全的……
my_list[-1] << 5 # ……但这里会失败
有两种广泛的方法可以向类型检查器声明子类型信息。
在名义子类型中,类型树基于类树,即 UserID 被视为 int 的子类型。这种方法应在类型检查器的控制下使用,因为在 Python 中可以以不兼容的方式覆盖属性:
class Base:
answer = '42' # 类型:str
class Derived(Base):
answer = 5 # 应该被类型检查器标记为错误
在结构子类型中,子类型关系是从声明的方法中推导出来的,即 UserID 和 int 将被视为同一种类型。虽然这可能会偶尔引起混淆,但结构子类型被认为更灵活。我们努力提供对这两种方法的支持,以便可以将结构信息与名义子类型一起使用。
渐进式类型的总结
渐进式类型允许只注解程序的一部分,从而结合动态类型和静态类型的优点。
我们定义了一种新的关系,即“一致”,它类似于“子类型”,但当涉及新类型 Any 时,它不是传递的。(两种关系都不是对称的。)如果 a_value 的类型与 a_variable 的类型一致,则可以将 a_value 赋值给 a_variable。(与“……如果 a_value 的类型是 a_variable 类型的子类型”相比,这是面向对象编程的一个基本原则。)“一致”关系由三条规则定义:
- 如果类型
t1是t2的子类型,则t1与t2一致。(但反过来不行。) Any与每种类型都一致。(但Any不是每种类型的子类型。)- 每种类型都与
Any一致。(但每种类型不是Any的子类型。)
仅此而已!请参阅 Jeremy Siek 的博客文章 什么是渐进式类型,以获得更长的解释和动机。Any 可以被视为具有所有值和所有方法的类型。结合上面的子类型定义,这使得 Any 部分位于类型层次结构的顶部(它拥有所有值)和底部(它拥有所有方法)。与 object 对比——它与大多数类型不一致(例如,你不能用 object() 实例代替期望的 int 类型)。换句话说,当用于注解参数时,Any 和 object 都意味着“允许任何类型”,但只有 Any 可以传递,无论期望什么类型(本质上,Any 声明了回退到动态类型并关闭静态检查器的抱怨)。
下面是一个示例,展示这些规则在实践中如何运作:
假设我们有一个 Employee 类和一个子类 Manager:
class Employee: ...
class Manager(Employee): ...
假设变量 worker 被声明为 Employee 类型:
worker = Employee() # 类型:Employee
现在可以将 Manager 实例赋值给 worker(规则 1):
worker = Manager() # OK
但不能将 Employee 实例赋值给声明为 Manager 类型的变量:
boss = Manager() # 类型:Manager
boss = Employee() # 静态检查失败
然而,假设我们有一个类型为 Any 的变量:
something = some_func() # 类型:Any
现在可以将 something 赋值给 worker(规则 2):
worker = something # OK
当然,也可以将 worker 赋值给 something(规则 3),但不需要“一致”概念来实现这一点:
类型与类
在 Python 中,类是由 class 语句定义的对象工厂,并通过内置函数 type(obj) 返回。类是一个动态的、运行时的概念。
类型的概念如上所述,类型出现在变量和函数的类型注解中,可以由下面描述的构建块构造,并且由静态类型检查器使用。
每个类都是一个类型,如上所述。但实现一个完全代表给定类型语义的类是棘手且容易出错的,这并不是 PEP 484 的目标。
PEP 484 中描述的静态类型不应与运行时类混淆。 示例:
-
int是一个类,也是一个类型。 -
UserID是一个类,也是一个类型。 -
Union[str, int]是一个类型,但不是一个类:class MyUnion(Union[str, int]): ... # 抛出 TypeError Union[str, int]() # 抛出 TypeError
类型接口是通过类实现的,即在运行时可以评估,例如,Generic[T].__bases__。但为了强调类和类型之间的区别,以下一般规则适用:
- 以下定义的类型(即
Any,Union等)不能被实例化,尝试这样做将引发TypeError。(但非抽象的Generic子类可以。) - 以下定义的类型不能被子类化,除了
Generic及其派生类。 - 如果它们出现在
isinstance或issubclass中(除了未参数化的泛型),将引发TypeError。
基本构建块
- Any。每种类型都与
Any一致;它也与每种类型一致(见上文)。 - Union[t1, t2, …]。至少是
t1等中的一种子类型的类型是这个类型的子类型。- 所有组件都是
t1等的子类型的联合也是这个类型的子类型。
示例: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 = [] # 类型:List[UserID] users.append(UserID(42)) # OK users.append('Some guy') # 应该被类型检查器拒绝 examples = {} # 类型:Dict[str, Any] examples['first example'] = object() # OK examples[2] = None # 被类型检查器拒绝 -
下面的函数可以接受两个
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: # 一个泛型函数
return seq[0]
accumulator = 0 # 类型:int
accumulator += take_first([1, 2, 3]) # 安全,T 推断为 int
accumulator += take_first((2.7, 3.5)) # 不安全
类型变量在类型注解中被广泛使用,类型检查器的类型推断内部机制通常也是基于类型变量构建的。因此,让我们详细考虑它们。
类型变量
X = TypeVar('X') 声明了一个唯一的类型变量。名称必须与变量名称匹配。默认情况下,类型变量的范围涵盖所有可能的类型。示例:
def do_nothing(one_arg: T, other_arg: T) -> None:
pass
do_nothing(1, 2) # OK,T 是 int
do_nothing('abc', UserID(42)) # 也可以,T 是 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') # 推断的结果类型是 str result = longest('a', b'abc') # 静态类型检查失败在这个示例中,
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'))推断的结果类型是
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')推断的结果类型仍然是
Union[str, bytes],即使两个参数都是str。注意,类型检查器会拒绝这个函数:
def concat(first: U, second: U) -> U: return first + second # 错误:不能连接 str 和 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 是泛型
...
class URLList(Iterable[bytes]):
def scrape_all(self) -> None:
...
def search(urls: URLList) -> Optional[bytes] # URLList 不是泛型
...
从泛型类型继承会在对应的特定类型之间施加子类型关系,因此在上面的例子中,TodoList[t1] 是 Iterable[t1] 的子类型。
泛型类型可以在多个步骤中进行特化(索引)。每个类型变量都可以被一个具体类型或另一个泛型类型替换。如果 Generic 出现在基类列表中,那么它应该包含所有类型变量,并且类型参数的顺序由它们在 Generic 中出现的顺序决定。示例:
Table = Dict[int, T] # Table 是泛型
Messages = Table[bytes] # 等同于 Dict[int, bytes]
class BaseGeneric(Generic[T, S]):
...
class DerivedGeneric(BaseGeneric[int, T]): # DerivedGeneric 有一个参数
...
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: # 等同于 Sequence[Any]
...
class FrameworkBase(Generic[S, T]):
...
class UserClass:
...
issubclass(UserClass, FrameworkBase) # 这是可以的
class Node(Generic[T]):
...
IntNode = Node[int]
my_node = IntNode() # 在运行时 my_node.__class__ 是 Node
# my_node 的推断静态类型是 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 什么也说不出。将 < 替换为“是子类型”,将函数替换为泛型类型构造器,我们就得到了协变、逆变和不变的例子。现在让我们考虑一些实际的例子:
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]的值集合的子集,但只能向List[int]中追加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]): # 默认是不变的
...
def append(self, element: T) -> None:
...
class Box(Generic[T_co]): # 声明为协变
def __init__(self, content: T_co) -> None:
self._content = content
def get_content(self) -> T_co:
return self._content
class Sink(Generic[T_contra]): # 声明为逆变
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] 的子类型。如果我们用 < 表示“是子类型”的关系,那么这个例子的完整子类型关系图将是:
Base[Manager] > Base[Employee]
v v
Derived[Manager] < Derived[Employee]
因此,类型检查器也会发现,例如,Derived[Manager] 是 Base[Employee] 的子类型。
关于类型变量、泛型类型和变体的更多信息,请参阅 PEP 484、mypy 文档中的 泛型 部分以及维基百科。
实用主义
有些事情与理论无关,但使实际使用更加方便。(这不是一个完整的列表;我可能遗漏了一些,有些仍然存在争议或尚未完全明确。)
-
在期望类型的地方,可以用
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 = [] # 类型: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]。
版权
本文件根据开放出版许可协议授权。
以上是 PEP 483 的完整翻译。如果你对某些部分有疑问,或者需要进一步的解释,请随时告诉我!
浙公网安备 33010602011771号