PEP 560 – 对 typing 模块和泛型类型的核心支持
- 作者:
- Ivan Levkivskyi <levkivskyi at gmail.com>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 主题:
- 类型标注
- 创建日期:
- 2017年9月3日
- Python 版本:
- 3.7
- 发布历史:
- 2017年9月9日,2017年11月14日
- 决议:
- Python-Dev 消息
摘要
最初 PEP 484 的设计方式是它不会对核心 CPython 解释器引入 任何 更改。现在类型提示和 typing 模块被社区广泛使用,例如 PEP 526 和 PEP 557 扩展了类型提示的使用,并且 PyPI 上的 typing 向后移植版本每月有 100 万次下载。因此,可以取消此限制。建议向核心 CPython 添加两个特殊方法 __class_getitem__ 和 __mro_entries__,以更好地支持泛型类型。
基本原理
不修改核心 CPython 解释器的限制导致了一些设计决策,当 typing 模块开始被广泛使用时,这些决策变得值得商榷。主要有三个关注点:typing 模块的性能、元类冲突以及 typing 中当前使用的大量 HACK。
性能
typing 模块是标准库中最重、最慢的模块之一,即使进行了所有优化也是如此。这主要是因为带下标的泛型类型(有关本 PEP 中使用的术语定义,请参阅 PEP 484)是类对象(另请参阅 [1])。在所提出的特殊方法的帮助下,可以通过三种主要方式提高性能:
- 泛型类的创建很慢,因为
GenericMeta.__new__非常慢;我们不再需要它。 - 泛型类的非常长的方法解析顺序(MRO)将减半;它们之所以存在,是因为我们在
typing中复制了collections.abc继承链。 - 泛型类的实例化将更快(但这只是次要的)。
元类冲突
所有泛型类型都是 GenericMeta 的实例,因此如果用户使用自定义元类,则很难使相应的类泛型化。对于用户无法控制的库类来说,这尤其困难。一种解决方法是始终混合 GenericMeta
class AdHocMeta(GenericMeta, LibraryMeta):
pass
class UserClass(LibraryBase, Generic[T], metaclass=AdHocMeta):
...
但这并不总是实用或甚至可能。借助所提出的特殊属性,将不再需要 GenericMeta 元类。
本提案将移除的 HACK 和 BUG
_generic_newHACK 之所以存在,是因为如果实例的类型与调用__new__的类型不同,则不会调用__init__,例如C[int]().__class__ is C。_next_in_mro速度 HACK 将不再需要,因为订阅将不再创建新类。- 丑陋的
sys._getframeHACK。这个尤其麻烦,因为看起来我们无法在不更改typing之外的代码的情况下移除它。 - 目前,泛型对私有 ABC 缓存进行危险操作,以修复至少以 O(N2) 增长的大量内存消耗,请参阅 [2]。这一点也很重要,因为最近有人提议用 C 重新实现
ABCMeta。 - 带下标的泛型之间共享属性的问题,请参阅 [3]。当前的解决方案已经使用了
__getattr__和__setattr__,但它仍然不完整,如果没有当前提案,解决这个问题将很困难,并且需要__getattribute__。 _no_slots_copyHACK,我们每次订阅时都会清理类字典,从而允许带有__slots__的泛型。typing模块的总体复杂性。新提案不仅允许移除上述 HACK/BUG,还会简化实现,从而使其更易于维护。
规范
__class_getitem__
__class_getitem__ 的思想很简单:它是 __getitem__ 的精确模拟,只是它是在定义它的类上调用,而不是在其实例上调用。这使我们能够避免像 Iterable[int] 这样的情况下的 GenericMeta.__getitem__。__class_getitem__ 自动成为类方法,并且不需要 @classmethod 装饰器(类似于 __init_subclass__),并且像普通属性一样继承。例如
class MyList:
def __getitem__(self, index):
return index + 1
def __class_getitem__(cls, item):
return f"{cls.__name__}[{item.__name__}]"
class MyOtherList(MyList):
pass
assert MyList()[0] == 1
assert MyList[int] == "MyList[int]"
assert MyOtherList()[0] == 1
assert MyOtherList[int] == "MyOtherList[int]"
请注意,此方法用作回退,因此如果元类定义了 __getitem__,则该方法将具有优先权。
__mro_entries__
如果一个不是类对象的对象出现在类定义的基本元组中,则会在其上搜索 __mro_entries__ 方法。如果找到,它将以原始的基本元组作为参数被调用。调用的结果必须是一个元组,该元组将在此对象的位置解包到基类中。(如果元组为空,则表示原始的基本元组被简单地丢弃。)如果有多个对象具有 __mro_entries__,则它们都将以相同的原始基本元组作为参数被调用。此步骤发生在类创建过程的最开始,所有其他步骤,包括检查重复基类和 MRO 计算,都将正常地使用更新后的基类进行。
使用方法 API 而不是仅使用属性是必要的,以避免不一致的 MRO 错误,并执行目前由 GenericMeta.__new__ 完成的其他操作。原始基类作为 __orig_bases__ 存储在类命名空间中(目前这也由元类完成)。例如
class GenericAlias:
def __init__(self, origin, item):
self.origin = origin
self.item = item
def __mro_entries__(self, bases):
return (self.origin,)
class NewList:
def __class_getitem__(cls, item):
return GenericAlias(cls, item)
class Tokens(NewList[int]):
...
assert Tokens.__bases__ == (NewList,)
assert Tokens.__orig_bases__ == (NewList[int],)
assert Tokens.__mro__ == (Tokens, NewList, object)
使用 __mro_entries__ 的解析 仅 发生在类定义语句的基类中。在所有其他预期类对象的情况下,不会发生此类解析,这包括 isinstance 和 issubclass 内置函数。
注意:这两个方法名保留用于 typing 模块和泛型类型机制,不鼓励任何其他用途。参考实现(带测试)可以在 [4] 中找到,并且该提案最初在 typing 跟踪器上发布和讨论,请参阅 [5]。
动态类创建和 types.resolve_bases
type.__new__ 将不执行任何 MRO 条目解析。因此直接调用 type('Tokens', (List[int],), {}) 将会失败。这样做是为了性能原因并最小化隐式转换的数量。相反,一个辅助函数 resolve_bases 将被添加到 types 模块中,以允许在动态类创建的上下文中进行显式的 __mro_entries__ 解析。相应地,types.new_class 将被更新以反映新的类创建步骤,同时保持向后兼容性
def new_class(name, bases=(), kwds=None, exec_body=None):
resolved_bases = resolve_bases(bases) # This step is added
meta, ns, kwds = prepare_class(name, resolved_bases, kwds)
if exec_body is not None:
exec_body(ns)
ns['__orig_bases__'] = bases # This step is added
return meta(name, resolved_bases, ns, **kwds)
在 C 扩展中使用 __class_getitem__
如上所述,如果 __class_getitem__ 在 Python 代码中定义,则它会自动成为类方法。要在 C 扩展中定义此方法,应使用标志 METH_O|METH_CLASS。例如,使扩展类泛型的一种简单方法是使用一个方法,该方法简单地返回原始类对象,从而在运行时完全擦除类型信息,并将所有检查推迟到静态类型检查器
typedef struct {
PyObject_HEAD
/* ... your code ... */
} SimpleGeneric;
static PyObject *
simple_class_getitem(PyObject *type, PyObject *item)
{
Py_INCREF(type);
return type;
}
static PyMethodDef simple_generic_methods[] = {
{"__class_getitem__", simple_class_getitem, METH_O|METH_CLASS, NULL},
/* ... other methods ... */
};
PyTypeObject SimpleGeneric_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
"SimpleGeneric",
sizeof(SimpleGeneric),
0,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_methods = simple_generic_methods,
};
此类可作为 Python 类型注解中的普通泛型使用(应为静态类型检查器提供相应的 stub 文件,详见 PEP 484)
from simple_extension import SimpleGeneric
from typing import TypeVar
T = TypeVar('T')
Alias = SimpleGeneric[str, T]
class SubClass(SimpleGeneric[T, int]):
...
data: Alias[int] # Works at runtime
more_data: SubClass[str] # Also works at runtime
向后兼容性及其对不使用 typing 的用户的影响
此提案可能会破坏目前使用名称 __class_getitem__ 和 __mro_entries__ 的代码。(但语言参考明确保留 所有 未记录的魔术方法名,并允许“不经警告地破坏”;请参阅 [6]。)
本提案将支持与当前公共泛型类型 API 几乎完全向后兼容;此外,typing 模块仍是 provisional 的。仅有的两个例外是,目前 issubclass(List[int], List) 返回 True,而根据本提案它将引发 TypeError,并且未带下标的用户定义泛型的 repr() 无法调整,并将与普通(非泛型)类的 repr() 一致。
通过参考实现,我测量了常规(非泛型)类的性能影响可以忽略不计(在微基准测试中低于 1%)。同时,泛型类的性能显著提升
importlib.reload(typing)快了多达 7 倍- 用户定义泛型类的创建速度提高了多达 4 倍(在空体的微基准测试中)
- 泛型类的实例化速度提高了多达 5 倍(在空
__init__的微基准测试中) - 泛型类型和实例的其他操作(如方法查找和
isinstance()检查)提高了约 10-20% - 目前概念验证实现中唯一变慢的方面是带下标的泛型缓存查找。然而,它已经非常高效,因此这个方面对整体影响可以忽略不计。
参考资料
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0560.rst
最后修改时间:2024-06-11 22:12:09 GMT