Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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

作者:
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 扩展了类型提示的使用,并且 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_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__ 的解析 发生在类定义语句的基类中。在所有其他预期类对象的情况下,不会发生此类解析,这包括 isinstanceissubclass 内置函数。

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