PEP 585 – 标准集合中的类型提示泛型
- 作者:
- Łukasz Langa <lukasz at python.org>
- 讨论至:
- Typing-SIG 邮件列表
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 主题:
- 类型标注
- 创建日期:
- 2019年3月3日
- Python 版本:
- 3.9
- 决议:
- Python-Dev 帖子
摘要
由 PEP 484、526、544、560 和 563 定义的静态类型是在现有 Python 运行时之上逐步构建的,并受到现有语法和运行时行为的限制。这导致 typing 模块中存在重复的集合层次结构,原因是泛型(例如 typing.List 和内置的 list)。
本 PEP 提议在当前 typing 模块中所有标准集合中启用对泛型语法的支持。
基本原理和目标
这一改变消除了 typing 模块中并行类型层次结构的必要性,使用户更容易注释他们的程序,也更容易让教师教授 Python。
术语
泛型(n.)——一种可以参数化的类型,通常是容器。也称为 *参数化类型* 或 *泛型类型*。例如:dict。
参数化泛型——泛型的一个特定实例,提供了容器元素的预期类型。也称为 *参数化类型*。例如:dict[str, int]。
向后兼容性
包括类型检查器和代码检查器在内的工具将需要适应以识别标准集合为泛型。
在源代码级别,新描述的功能需要 Python 3.9。对于仅限于类型注释的用例,带有 “annotations” 未来导入(自 Python 3.7 起可用)的 Python 文件可以参数化标准集合,包括内置类型。重申一下,这取决于外部工具理解这是有效的。
实施
从 Python 3.7 开始,当使用 from __future__ import annotations 时,函数和变量注释可以直接参数化标准集合。示例:
from __future__ import annotations
def find(haystack: dict[str, list[int]]) -> int:
...
在 PEP 585 之前,这种语法的实用性有限,因为 Mypy 等外部工具不将标准集合识别为泛型。此外,某些类型特性,如类型别名或强制转换,需要在注释之外的运行时上下文中放置类型。虽然这些相对不如类型注释常见,但重要的是允许在所有上下文中使用相同的类型语法。这就是为什么从 Python 3.9 开始,以下集合使用 __class_getitem__() 来参数化包含的类型,从而成为泛型:
tuple# typing.Tuplelist# typing.Listdict# typing.Dictset# typing.Setfrozenset# typing.FrozenSettype# typing.Typecollections.dequecollections.defaultdictcollections.OrderedDictcollections.Countercollections.ChainMapcollections.abc.Awaitablecollections.abc.Coroutinecollections.abc.AsyncIterablecollections.abc.AsyncIteratorcollections.abc.AsyncGeneratorcollections.abc.Iterablecollections.abc.Iteratorcollections.abc.Generatorcollections.abc.Reversiblecollections.abc.Containercollections.abc.Collectioncollections.abc.Callablecollections.abc.Set# typing.AbstractSetcollections.abc.MutableSetcollections.abc.Mappingcollections.abc.MutableMappingcollections.abc.Sequencecollections.abc.MutableSequencecollections.abc.ByteStringcollections.abc.MappingViewcollections.abc.KeysViewcollections.abc.ItemsViewcollections.abc.ValuesViewcontextlib.AbstractContextManager# typing.ContextManagercontextlib.AbstractAsyncContextManager# typing.AsyncContextManagerre.Pattern# typing.Pattern, typing.re.Patternre.Match# typing.Match, typing.re.Match
从 typing 导入这些已被弃用。由于 PEP 563 以及最大程度减少类型对运行时影响的意图,此弃用不会生成 DeprecationWarnings。相反,当被检查程序的目標版本被标记为 Python 3.9 或更高版本时,类型检查器可能会对这种弃用用法发出警告。建议允许在项目范围内取消这些警告。
此弃用功能最终可能会从 typing 模块中移除。移除将不早于 Python 3.9 的生命周期结束,计划于 2025 年 10 月。
泛型的参数在运行时可用
在运行时保留泛型类型可以实现类型内省,这可用于 API 生成或运行时类型检查。这种用法已经存在于实际应用中。
就像今天的 typing 模块一样,上一节中列出的所有参数化泛型类型都在运行时保留其类型参数:
>>> list[str]
list[str]
>>> tuple[int, ...]
tuple[int, ...]
>>> ChainMap[str, list[str]]
collections.ChainMap[str, list[str]]
这是通过一个薄代理类型实现的,该代理类型将所有方法调用和属性访问转发给裸源类型,但以下情况除外:
__repr__显示参数化类型;__origin__属性指向非参数化泛型类;__args__属性是传递给原始__class_getitem__的泛型类型的元组(可能长度为 1);__parameters__属性是__args__中找到的唯一类型变量的惰性计算元组(可能为空);__getitem__会引发异常以防止类似dict[str][str]的错误。但是,它允许例如dict[str, T][int],在这种情况下会返回dict[str, int]。
这种设计意味着可以创建参数化集合的实例,例如
>>> l = list[str]()
[]
>>> list is list[str]
False
>>> list == list[str]
False
>>> list[str] == list[str]
True
>>> list[str] == list[int]
False
>>> isinstance([1, 2, 3], list[str])
TypeError: isinstance() arg 2 cannot be a parameterized generic
>>> issubclass(list, list[str])
TypeError: issubclass() arg 2 cannot be a parameterized generic
>>> isinstance(list[str], types.GenericAlias)
True
使用裸类型和参数化类型创建的对象完全相同。泛型参数不会在用参数化类型创建的实例中保留,换句话说,泛型类型在对象创建期间会擦除类型参数。
这一个重要的结果是,解释器**不**会尝试对使用参数化类型创建的集合上的操作进行类型检查。这在以下两者之间提供了对称性:
l: list[str] = []
和
l = list[str]()
为了从 Python 代码访问代理类型,它将从 types 模块作为 GenericAlias 导出。
对 GenericAlias 实例进行腌制或(浅复制或深复制)将保留类型、来源、属性和参数。
前向兼容性
未来的标准集合必须实现相同的行为。
参考实现
存在一个概念验证或原型实现。
被拒绝的替代方案
不作为
维持现状迫使 Python 程序员对标准集合从 typing 模块的导入进行簿记,使得除最简单的注释之外的所有注释都难以维护。并行类型的存在令新手感到困惑(为什么既有 list 又有 List?)。
上述问题在用户构建的泛型类中也不存在,这些类共享运行时功能和将其用作泛型类型注释的能力。使标准集合在用户类中进行类型提示时难以使用,这阻碍了类型采用和可用性。
泛型擦除
在列出的标准集合上以不保留泛型类型的方式实现 __class_getitem__ 会更容易,换句话说:
>>> list[str]
<class 'list'>
>>> tuple[int, ...]
<class 'tuple'>
>>> collections.ChainMap[str, list[str]]
<class 'collections.ChainMap'>
这是有问题的,因为它破坏了向后兼容性:typing 模块中这些类型的当前等效项**确实**保留了泛型类型:
>>> from typing import List, Tuple, ChainMap
>>> List[str]
typing.List[str]
>>> Tuple[int, ...]
typing.Tuple[int, ...]
>>> ChainMap[str, List[str]]
typing.ChainMap[str, typing.List[str]]
正如“实现”一节所述,在运行时保留泛型类型可以实现类型内省,这可用于 API 生成或运行时类型检查。这种用法已经存在于实际应用中。
此外,将下标实现为恒等函数会使 Python 对初学者不那么友好。例如,如果用户错误地将列表类型而不是列表对象传递给函数,并且该函数对接收到的对象进行索引,则代码将不再引发错误。
今天
>>> l = list
>>> l[-1]
TypeError: 'type' object is not subscriptable
当 __class_getitem__ 作为恒等函数时
>>> l = list
>>> l[-1]
list
这里的索引成功很可能会在远处引发异常,从而使用户感到困惑。
不允许实例化参数化类型
鉴于保留 __origin__ 和 __args__ 的代理类型主要用于运行时内省目的,我们可能不允许实例化参数化类型。
事实上,禁止实例化参数化类型是 typing 模块目前对与内置集合并行的类型所做的事情(允许实例化其他参数化类型)。
这个决定的最初原因是阻止了不必要的参数化,这使得对象创建比内置集合可用的特殊语法慢了两个数量级。
这个理由不足以允许对内置类型进行特殊处理。所有其他参数化类型都可以实例化,包括标准库中集合的并行类型。此外,Python 允许使用 list() 实例化列表,并且某些内置集合不提供特殊的实例化语法。
使 isinstance(obj, list[str]) 执行忽略泛型的检查
本 PEP 的早期版本建议将 list[str] 这样的参数化泛型视为与 list 这样的非参数化变体等效,用于 isinstance() 和 issubclass() 的目的。这将与 list[str]() 如何创建常规列表对称。
这个设计被拒绝了,因为带有参数化泛型的 isinstance() 和 issubclass() 检查读起来会像是逐元素运行时类型检查。这些检查的结果会令人惊讶,例如:
>>> isinstance([1, 2, 3], list[str])
True
请注意,该对象不匹配提供的泛型类型,但 isinstance() 仍返回 True,因为它只检查对象是否是列表。
如果一个库遇到一个参数化泛型,并且希望使用基类型执行 isinstance() 检查,那么该类型可以通过参数化泛型上的 __origin__ 属性来检索。
使 isinstance(obj, list[str]) 执行运行时类型检查
此功能需要遍历集合,这在某些集合中是一个破坏性操作。此功能本会有用,但是,在本 PEP 的范围内,实现在 Python 内部处理复杂类型、嵌套类型检查、类型变量、字符串前向引用等的类型检查器超出了范围。
将类型命名为 GenericType 而不是 GenericAlias
我们考虑过为这种类型使用不同的名称,但决定 GenericAlias 更好——它们不是真正的类型,它们是相应容器类型的别名,附带了一些额外的元数据。
关于初稿的说明
本 PEP 的早期版本讨论了标准集合中泛型之外的事项。为了清晰起见,那些不相关的主题已被删除。
致谢
感谢 Guido van Rossum 对 Python 的工作,特别是对本 PEP 的实现。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0585.rst