Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python增强提案

PEP 560 – 类型模块和泛型类型的核心支持

作者:
Ivan Levkivskyi <levkivskyi at gmail.com>
状态:
最终
类型:
标准跟踪
主题:
类型提示
创建:
2017年9月3日
Python版本:
3.7
历史记录:
2017年9月9日,2017年11月14日
决议:
Python-Dev邮件

目录

重要

此PEP是一个历史文档。最新的规范文档现在可以在__class_getitem__()__mro_entries__()的文档中找到。

×

请参阅PEP 1,了解如何提出更改。

摘要

最初PEP 484的设计方式是,它不会对核心CPython解释器进行*任何*更改。现在,类型提示和typing模块被社区广泛使用,例如PEP 526PEP 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_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__进行解析*仅*发生在类定义语句的基类中。在所有其他期望类对象的情况下,都不会发生此类解析,这包括isinstanceissubclass内置函数。

注意:这两个方法名称保留供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