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” future-import(从 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.deque
collections.defaultdict
collections.OrderedDict
collections.Counter
collections.ChainMap
collections.abc.Awaitable
collections.abc.Coroutine
collections.abc.AsyncIterable
collections.abc.AsyncIterator
collections.abc.AsyncGenerator
collections.abc.Iterable
collections.abc.Iterator
collections.abc.Generator
collections.abc.Reversible
collections.abc.Container
collections.abc.Collection
collections.abc.Callable
collections.abc.Set
# typing.AbstractSetcollections.abc.MutableSet
collections.abc.Mapping
collections.abc.MutableMapping
collections.abc.Sequence
collections.abc.MutableSequence
collections.abc.ByteString
collections.abc.MappingView
collections.abc.KeysView
collections.abc.ItemsView
collections.abc.ValuesView
contextlib.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
上次修改时间:2024-06-11 22:12:09 GMT