PEP 422 – 简化类创建的自定义
- 作者:
- Alyssa Coghlan <ncoghlan at gmail.com>, Daniel Urban <urban.dani+py at gmail.com>
- 状态:
- 已撤回
- 类型:
- 标准跟踪
- 创建:
- 2012年6月5日
- Python 版本:
- 3.5
- 历史记录:
- 2012年6月5日,2013年2月10日
摘要
目前,自定义类创建需要使用自定义元类。然后,此自定义元类在类的整个生命周期中持续存在,从而可能导致虚假的元类冲突。
本 PEP 建议改用类头中的新 namespace
参数和类体中的新 __autodecorate__
钩子来支持各种自定义场景。
与实现自定义元类相比,新机制更容易理解和使用,因此应该为充分利用 Python 的元类机制提供更轻松的入门。
PEP 撤回
此提案已被撤回,转而支持 Martin Teichmann 在 PEP 487 中的提案,该提案通过一个更简单、更容易使用的 __init_subclass__
钩子实现了相同目标,该钩子只是没有为定义钩子的基类调用。
背景
对于已经创建的类 cls
,“元类”一词具有明确的含义:它是 type(cls)
的值。
在类创建期间,它具有另一个含义:它也用于指代作为类定义的一部分可能提供的元类提示。虽然在许多情况下,这两个含义最终都指代同一个对象,但有两种情况并非如此
- 如果元类提示引用
type
的实例,则将其视为候选元类,以及正在定义的类的所有父类的元类。如果在候选者中找到更合适的元类,则将使用它而不是元类提示中给出的元类。 - 否则,假定显式元类提示是工厂函数,并直接调用它以创建类对象。在这种情况下,最终元类将由工厂函数定义确定。在典型情况下(其中工厂函数仅调用
type
,或者在 Python 3.3 或更高版本中调用types.new_class
),则根据父类确定实际元类。
值得注意的是,只有实际的元类被继承 - 用作元类钩子的工厂函数只看到当前正在定义的类,并且不会为任何子类调用。
在 Python 3 中,元类提示使用类头中的 metaclass=Meta
关键字语法提供。这允许元类上的 __prepare__
方法用于创建在执行类体期间使用的 locals()
命名空间(例如,指定使用 collections.OrderedDict
而不是常规的 dict
)。
在 Python 2 中,没有 __prepare__
方法(该 API 是由 PEP 3115 为 Python 3 添加的)。相反,类体可以设置 __metaclass__
属性,并且类创建过程将从类命名空间中提取该值以用作元类提示。有 已发布的代码 使用此功能。
Python 3 中的另一个新功能是 PEP 3135 引入的 super()
内置函数的零参数形式。此功能使用对正在定义的类的隐式 __class__
引用来替换 Python 2 中所需的“按名称”引用。就像在 Python 2 元类的执行期间调用的代码无法调用按名称引用类的的方法(因为名称尚未在包含作用域中绑定)一样,类似地,Python 3 元类无法调用依赖于隐式 __class__
引用的方法(因为它在元类将控制权返回给类创建机制后才填充)。
最后,当类使用自定义元类时,它可能会对使用多重继承造成额外的挑战,因为新类无法继承具有不相关元类的父类。这意味着不可能向已发布的类添加元类:由于存在元类冲突的风险,这种添加是不兼容的更改。
提案
本 PEP 建议向 Python 3.4 添加一种自定义类创建的新机制,以满足以下条件
- 与类继承结构(包括 mixin 和多重继承)很好地集成
- 与 PEP 3135 引入的隐式
__class__
引用和零参数super()
语法很好地集成 - 可以添加到现有的基类中,而不会出现引入向后兼容性问题的重大风险
- 恢复类命名空间对类创建过程具有一定影响的能力(超出填充命名空间本身),但可能没有 Python 2 样式
__metaclass__
钩子的全部灵活性
可以实现此目标的一种机制是添加一个新的隐式类装饰钩子,该钩子直接模拟现有的显式类装饰器,但在类体或父类中定义,而不是作为类定义头的一部分。
具体来说,建议类定义能够提供如下类初始化钩子
class Example:
def __autodecorate__(cls):
# This is invoked after the class is created, but before any
# explicit decorators are called
# The usual super() mechanisms are used to correctly support
# multiple inheritance. The class decorator style signature helps
# ensure that invoking the parent class is as simple as possible.
cls = super().__autodecorate__()
return cls
为了简化协作多重继承的情况, object
将获得钩子的默认实现,该实现返回未修改的类
class object:
def __autodecorate__(cls):
return cls
如果元类出于某种原因希望阻止隐式类装饰,则必须安排 cls.__autodecorate__
触发 AttributeError
。
如果在创建的对象上存在,则此新钩子将在 __class__
引用初始化之后由类创建机制调用。对于 types.new_class()
,它将在返回创建的类对象之前的最后一步调用。 __autodecorate__
在类创建时(在调用钩子之前)隐式转换为类方法。
请注意,当调用 __autodecorate__
时,类的名称尚未绑定到新的类对象。因此, super()
的双参数形式不能用于调用方法(例如,在上面的示例中 super(Example, cls)
将不起作用)。但是, super()
的零参数形式按预期工作,因为 __class__
引用已初始化。
这个通用提案并不是一个新想法(它在 10 多年前 就首次建议将其包含在语言定义中,并且 Zope 的 ExtensionClass 长期以来一直支持类似的机制),但近年来情况已经发生了足够的变化,因此值得重新考虑将其作为原生语言特性包含在内。
此外,PEP 3115 中元类 __prepare__
方法的引入允许进一步增强 Python 2 中无法实现的功能:本 PEP 还建议更新 type.__prepare__
以接受工厂函数作为 namespace
关键字仅参数。如果存在,则将调用作为 namespace
参数提供的值(不带参数)以创建 type.__prepare__
的结果,而不是使用新创建的字典实例。例如,以下内容将使用有序字典作为类命名空间
class OrderedExample(namespace=collections.OrderedDict):
def __autodecorate__(cls):
# cls.__dict__ is still a read-only proxy to the class namespace,
# but the underlying storage is an OrderedDict instance
注意
本 PEP 以及现有的使用 `__prepare__` 在多个类对象之间共享单个命名空间的功能,突出了属性查找缓存的一个潜在问题:当底层映射通过其他方式更新时,属性查找缓存不会正确失效(这是类 `__dict__` 属性生成底层存储的只读视图的关键原因之一)。
由于该缓存提供的优化非常理想,因此将预先存在的命名空间用作类命名空间可能需要声明为官方不支持(因为当缓存不同步时,观察到的行为相当奇怪)。
主要优势
更轻松地使用类的自定义命名空间
目前,要为类命名空间使用不同的类型(例如 `collections.OrderedDict`),或使用预填充的命名空间,需要编写和使用自定义元类。使用本 PEP,使用自定义命名空间变得像在类头中指定适当的工厂函数一样简单。
更轻松地继承定义时行为
理解 Python 的元类需要深入理解类型系统和类构造过程。由于需要在你的脑海中清晰地区分多个活动部件(代码、元类提示、实际元类、类对象、类对象的实例),这被认为是具有挑战性的。即使你了解规则,如果你不小心,也很容易犯错。本 PEP 的早期版本实际上包含了这样一个错误:它将实际上是“type 的实例”的约束声明为“type 的子类”。
理解提议的隐式类装饰器钩子只需要理解装饰器和普通方法继承,这并不是一项令人生畏的任务。新的钩子提供了一条更渐进的路径来理解类定义过程中涉及的所有阶段。
减少元类冲突的可能性
使库作者不愿使用元类(即使它们适用)的主要问题之一是元类冲突的风险。每当类定义所需的父类使用两个不相关的元类时,就会发生这种情况。这种风险也使得在之前没有元类的类中添加元类变得非常困难。
相比之下,向现有类型添加 `__autodecorate__` 方法带来的风险与添加 `__init__` 方法类似:从技术上讲,存在破坏实现不当的子类的风险,但当发生这种情况时,它被认为是子类中的错误,而不是库作者违反向后兼容性保证。事实上,由于 `__autodecorate__` 的签名受限,在这种情况下,风险实际上甚至低于 `__init__` 的情况。
与 PEP 3135 无缝集成
与作为元类一部分运行的代码不同,作为新钩子一部分运行的代码将能够自由地调用依赖于 `PEP 3135 <../pep-3135/>` 引入的隐式 `__class__` 引用(包括使用 `super()` 的零参数形式的方法)的类方法。
替换 __metaclass__
的许多动态设置用例
对于不涉及完全替换已定义类的用例,现在可以使用 Python 2 代码动态设置 `__autodecorate__` 而不是动态设置 `__metaclass__`。对于更高级的用例,仍然需要引入显式元类(可能作为必需的基类提供)以支持 Python 3。
设计说明
确定正在装饰的类是否为基类
在 `__autodecorate__` 方法的主体中,与任何其他类方法一样,`__class__` 将绑定到声明该方法的类,而传入的值可能是子类。
这使得在必要时跳过处理基类变得相对简单。
class Example:
def __autodecorate__(cls):
cls = super().__autodecorate__()
# Don't process the base class
if cls is __class__:
return
# Process subclasses here
...
用不同类型的对象替换类
作为隐式装饰器,`__autodecorate__` 能够相对轻松地用其他类型的对象替换已定义的类。从技术上讲,自定义元类甚至 `__new__` 方法已经可以隐式地做到这一点,但装饰器模型使此类代码更容易理解和实现。
class BuildDict:
def __autodecorate__(cls):
cls = super().__autodecorate__()
# Don't process the base class
if cls is __class__:
return
# Convert subclasses to ordinary dictionaries
return cls.__dict__.copy()
目前尚不清楚为什么有人会基于继承隐式地执行此操作,而不是仅仅使用显式装饰器,但这种可能性似乎值得注意。
未解决的问题
namespace
概念是否值得额外的复杂性?
与新的 `__autodecorate__` 钩子不同,提议的 `namespace` 关键字参数不会被子类自动继承。鉴于本提案目前的写法,获得在子类中始终一致使用的特殊命名空间的唯一方法仍然是编写具有合适的 `__prepare__` 实现的自定义元类。
更改自定义命名空间工厂以使其也继承将大大增加本提案的复杂性,并引入与使用自定义元类时出现的一些相同的潜在基类冲突问题。
Eric Snow 提出了一个 单独的提案,建议将类体的执行命名空间默认设置为有序字典,并将类属性定义顺序作为属性(例如 `__definition_order__`)捕获到类对象上。
Eric 的建议方法可能是 type 的新默认行为的更好选择,它与提议的 `__autodecorate__` 钩子很好地结合,将更复杂的可配置命名空间工厂的想法留给像下面所示的自定义元类。
使用类的新方法
类头中的新 `namespace` 关键字为控制类初始化的方式(包括 Javascript 和 Ruby 对象模型的一些方面)提供了许多有趣的选择。
以下所有示例实际上都可以通过使用自定义元类来实现。
class CustomNamespace(type):
@classmethod
def __prepare__(meta, name, bases, *, namespace=None, **kwds):
parent_namespace = super().__prepare__(name, bases, **kwds)
return namespace() if namespace is not None else parent_namespace
def __new__(meta, name, bases, ns, *, namespace=None, **kwds):
return super().__new__(meta, name, bases, ns, **kwds)
def __init__(cls, name, bases, ns, *, namespace=None, **kwds):
return super().__init__(name, bases, ns, **kwds)
直接在 `type.__prepare__` 中实现新关键字的优势在于,唯一持久的效应就是类属性底层存储的变化。类的元类保持不变,消除了通常与这些类型的自定义相关的许多缺点。
保持顺序的类
class OrderedClass(namespace=collections.OrderedDict):
a = 1
b = 2
c = 3
预填充的命名空间
seed_data = dict(a=1, b=2, c=3)
class PrepopulatedClass(namespace=seed_data.copy):
pass
克隆原型类
class NewClass(namespace=Prototype.__dict__.copy):
pass
扩展类
注意
仅仅因为 PEP 使得相对干净地执行此操作成为可能,并不意味着任何人应该这样做!
from collections import MutableMapping
# The MutableMapping + dict combination should give something that
# generally behaves correctly as a mapping, while still being accepted
# as a class namespace
class ClassNamespace(MutableMapping, dict):
def __init__(self, cls):
self._cls = cls
def __len__(self):
return len(dir(self._cls))
def __iter__(self):
for attr in dir(self._cls):
yield attr
def __contains__(self, attr):
return hasattr(self._cls, attr)
def __getitem__(self, attr):
return getattr(self._cls, attr)
def __setitem__(self, attr, value):
setattr(self._cls, attr, value)
def __delitem__(self, attr):
delattr(self._cls, attr)
def extend(cls):
return lambda: ClassNamespace(cls)
class Example:
pass
class ExtendedExample(namespace=extend(Example)):
a = 1
b = 2
c = 3
>>> Example.a, Example.b, Example.c
(1, 2, 3)
被拒绝的设计选项
从 type.__init__
调用 __autodecorate__
从 `type.__init__` 自动调用新钩子可以实现本 PEP 的大部分目标。但是,使用这种方法意味着 `__autodecorate__` 实现将无法调用任何依赖于 `__class__` 引用(或使用 `super()` 的零参数形式)的方法,并且本身也无法使用这些功能。
当前的设计确保隐式装饰器钩子能够执行显式装饰器可以执行的任何操作,方法是在初始类创建完成后运行它。
调用自动装饰钩子 __init_class__
PEP 的早期版本使用名称 `__init_class__` 作为新钩子的名称。这个名称有三个重要的问题。
- 很难记住正确的拼写是 `__init_class__` 还是 `__class_init__`。
- 名称中使用“init”表明签名应该与 `type.__init__` 的签名匹配,但事实并非如此。
- 名称中使用“init”表明该方法将在类对象创建过程中运行,但事实并非如此。
选择新的名称 `__autodecorate__` 是为了清楚地表明,新的初始化钩子最有用地被认为是隐式调用的类装饰器,而不是像 `__init__` 方法。
要求在 __autodecorate__
上使用显式装饰器
最初,本 PEP 要求在 `__autodecorate__` 装饰器上显式使用 `@classmethod`。它被设为隐式,因为没有合理的解释可以省略它,并且无论如何都需要检测这种情况以提供有用的错误消息。
在注意到定义 `__prepare__` 并忘记 `@classmethod` 方法装饰器的用户体验是完全难以理解的之后,这一决定得到了加强(特别是由于 `PEP 3115 <../pep-3115/>` 将其记录为普通方法,并且当前文档没有明确说明任何一方)。
使 __autodecorate__
隐式静态,如 __new__
虽然它接受要实例化的类作为第一个参数,但 `__new__` 实际上被隐式地视为静态方法而不是类方法。这使得它可以很容易地从其定义类中提取并直接在子类上调用,而不是与它从中检索的类对象耦合。
这种行为最初似乎对新的 `__autodecorate__` 钩子可能有用,因为它允许 `__autodecorate__` 方法很容易地用作其他类的显式装饰器。
但是,这种明显的支持将是一种错觉,因为它仅在子类上调用时才能正常工作,在这种情况下,该方法可以同样容易地从子类中检索并以这种方式调用。与 `__new__` 不同,在继承链的不同点更改方法签名没有任何问题。
直接传入命名空间而不是工厂函数
在某一时刻,本 PEP 建议将类命名空间直接作为关键字参数传递,而不是传递工厂函数。但是,这鼓励了一种不受支持的行为(即,将相同的命名空间传递给多个类,或保留对用作类命名空间的映射的直接写入访问权限),因此 API 切换到工厂函数版本。
参考实现
`__autodecorate__` 的参考实现已发布到 问题跟踪器。它使用原始的 `__init_class__` 命名。尚不允许隐式装饰器用不同的对象替换类,并且没有实现建议的 `type.__prepare__` 的 `namespace` 参数。
待办事项
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0422.rst