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'
都属于此类型,因为可以在它们上面调用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]
上添加一个实数才能工作,因此第二个条件不成立。
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
将被认为是相同的类型。虽然这可能会偶尔导致混淆,但结构化子类型化被认为更灵活。我们努力为这两种方法提供支持,以便结构化信息可以与名义子类型化一起使用。
逐步类型化的总结
逐步类型化允许用户仅对程序的一部分进行注释,从而利用动态类型化和静态类型化的理想方面。
我们定义了一种新的关系,即与…一致,它类似于子类型关系,但当涉及到新的类型 Any
时,它不具有传递性。(两种关系都不是对称的。)将 a_value
赋值给 a_variable
是可以的,如果 a_value
的类型与 a_variable
的类型一致。(将此与“如果 a_value
的类型是 a_variable
的类型的子类型”,它陈述了 OO 编程的基本原理之一。)与…一致的关系由三个规则定义
- 如果
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
一致;它也与每个类型一致(见上文)。 - 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 = [] # 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
,则返回类型也必须是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')
result
的推断类型仍然是Union[str, bytes]
,即使两个参数都是str
。请注意,类型检查器将拒绝此函数
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
被称为
- 协变,如果
GenType[t2]
是GenType[t1]
的子类型,对于所有这样的t1
和t2
。 - 逆变,如果
GenType[t1]
是GenType[t2]
的子类型,对于所有这样的t1
和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
在其所有参数中都表现为协变。实际上,如上所述,Union[t1, t2, ...]
是Union[u1, u2, ...]
的子类型,如果t1
是u1
的子类型,等等。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]
的子类型。如果我们用 <
表示是子类型关系,那么这种情况下子类型的完整图将是
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 = [] # 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]
。
版权
本文件根据 开放出版许可证 授权。
参考文献和脚注
来源:https://github.com/python/peps/blob/main/peps/pep-0483.rst
上次修改时间:2024-02-11 04:14:42 GMT