PEP 560 – 类型模块和泛型类型的核心支持
- 作者:
- 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扩展了类型提示的使用,并且typing
的PyPI反向移植每月有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_new
Hack的存在是因为在与调用__new__
的类型的类型不同的实例上不会调用__init__
,C[int]().__class__ is C
。_next_in_mro
速度Hack将不再必要,因为订阅不会创建新类。- 丑陋的
sys._getframe
Hack。这个特别讨厌,因为看起来我们无法在不进行typing
外部更改的情况下将其移除。 - 目前,泛型对私有的ABC缓存执行了危险的操作,以修复至少以O(N2)增长的内存消耗问题,请参阅[2]。这一点也很重要,因为最近有人提议用C重新实现
ABCMeta
。 - 在带下标的泛型之间共享属性的问题,请参阅[3]。当前的解决方案已经使用了
__getattr__
和__setattr__
,但它仍然不完整,并且在没有当前提案的情况下解决这个问题将很困难,并且需要__getattribute__
。 _no_slots_copy
Hack,我们在每次订阅时都会清理类字典,从而允许使用__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],), {})
将失败。这样做是为了提高性能并最大程度地减少隐式转换的数量。相反,将向types
模块添加一个辅助函数resolve_bases
,以允许在动态类创建的上下文中显式__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__
如上所述,如果在Python代码中定义了__class_getitem__
,则它会自动成为类方法。要在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类型注释中用作普通泛型(应为静态类型检查器提供相应的存根文件,有关详细信息,请参阅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
模块仍处于试验阶段。仅有的两个例外是,目前 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