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 的另一个新特性是 super() 内置函数的零参数形式,由 PEP 3135 引入。此特性使用对正在定义的类的隐式 __class__ 引用来取代 Python 2 中所需的“按名称”引用。就像在 Python 2 元类执行期间调用的代码不能调用通过名称引用类的方法一样(因为该名称尚未在包含作用域中绑定),同样,Python 3 元类也不能调用依赖于隐式 __class__ 引用的方法(因为它在元类将控制权返回给类创建机制之前不会被填充)。
最后,当一个类使用自定义元类时,它可能会给多重继承带来额外的挑战,因为一个新类不能继承自具有不相关元类的父类。这意味着不可能向一个已经发布的类添加一个元类:这种添加是向后不兼容的更改,因为存在元类冲突的风险。
提案
本PEP提议在Python 3.4中添加一个新的定制类创建机制,该机制应满足以下标准:
- 与类继承结构(包括混入和多重继承)良好集成
- 与 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__ 引用已经初始化。
这个通用提案并非新想法(它首次被建议纳入语言定义 十多年前,并且Zope的ExtensionClass长期支持类似机制 Zope’s 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 引入的隐式 __class__ 引用的类方法,包括使用零参数形式的 super() 的方法。
取代了许多动态设置 __metaclass__ 的用例
对于不涉及完全替换定义类的情况,Python 2 中动态设置 __metaclass__ 的代码现在可以动态设置 __autodecorate__。对于更高级的用例,为了支持 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 将其记录为普通方法,而当前文档没有明确说明任何一种情况)之后,这个决定得到了加强。
使 __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
最后修改: 2025-02-01 08:59:27 GMT