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

Python 增强提案

PEP 3115 – Python 3000 中的元类

作者:
Talin <viridia at gmail.com>
状态:
最终
类型:
标准跟踪
创建:
2007 年 3 月 7 日
Python 版本:
3.0
历史记录:
2007 年 3 月 11 日,2007 年 3 月 14 日

目录

摘要

本 PEP 提出更改声明元类的语法,并改变具有元类的类的构造语义。

基本原理

本 PEP 有两个基本原理,两者都比较微妙。

更改元类工作方式的主要原因是,许多有趣的用例都需要元类在类构造过程中比目前可能更早地参与进来。目前,元类机制本质上是一个后处理步骤。随着类装饰器的出现,装饰器机制可以接管许多后处理任务。

特别是,在许多重要的用例中,保留类成员声明的顺序将很有用。普通的 Python 对象将它们的成员存储在一个字典中,在这个字典中顺序并不重要,并且成员严格按名称访问。但是,Python 通常用于与外部系统接口,这些系统中的成员按照隐式顺序组织。例如,声明 C 结构体;COM 对象;将 Python 类自动转换为 IDL 或数据库模式,例如 ORM 中使用;等等。

在这些情况下,Python 程序员可以使用类成员的声明顺序直接指定此类顺序将很有用。目前,这种顺序必须使用其他机制显式指定(参见 ctypes 模块以了解示例)。

不幸的是,当前声明元类的方法不允许这样做,因为在元类发挥作用时,顺序信息已经丢失。通过允许元类更早地参与类构造过程,新系统允许保留和检查构造的顺序或其他早期工件。

提出的元类机制还支持许多其他有趣的用例,这些用例超出了保留声明顺序的范围。一种用例是在类体名称空间中插入仅在类构造期间有效的符号。一个例子可能是“字段构造器”,这些是用于创建类成员的小函数。另一个有趣的方法是支持前向引用,即对在类体中进一步声明的 Python 符号的引用。

另一个较弱的基本原理纯粹是美观的:当前指定元类的方法是将其分配给特殊变量 __metaclass__,有些人认为这在审美上不尽如人意。其他人则强烈反对这种观点。本 PEP 不会解决这个问题,只会提到它,因为审美上的争论无法通过逻辑证明来解决。

规范

在新模型中,指定元类的语法是通过基类列表中的关键字参数来实现的

class Foo(base1, base2, metaclass=mymeta):
    ...

这里也将允许使用其他关键字,并将像下面的示例一样传递给元类

class Foo(base1, base2, metaclass=mymeta, private=True):
    ...

请注意,本 PEP 并没有试图定义这些其他关键字可能是什么——这取决于元类实现者来决定。

更一般地说,现在传递给类定义的参数列表将支持函数调用的所有功能,这意味着你现在可以在类基类列表中使用 *args**kwargs 样式参数

class Foo(*bases, **kwds):
    ...

调用元类

在当前的元类系统中,元类对象可以是任何可调用类型。这不会改变,但是为了充分利用所有新功能,元类将需要有一个额外的属性,该属性在类预构造期间使用。

此属性名为 __prepare__,它在类体求值之前作为一个函数被调用。 __prepare__ 函数接受两个位置参数和任意数量的关键字参数。这两个位置参数是

名称 正在创建的类的名称。
基类 基类列表。

解释器始终在调用它之前测试 __prepare__ 的存在性;如果它不存在,则使用常规字典,如以下 Python 代码片段所示。

def prepare_class(name, *bases, metaclass=None, **kwargs):
    if metaclass is None:
        metaclass = compute_default_metaclass(bases)
    prepare = getattr(metaclass, '__prepare__', None)
    if prepare is not None:
        return prepare(name, bases, **kwargs)
    else:
        return dict()

上面的示例说明了对 ‘class’ 的参数如何解释。类名是第一个参数,后面跟着任意长度的基类列表。在基类之后,可能有一个或多个关键字参数,其中一个可以是 metaclass。请注意,metaclass 参数不包含在 kwargs 中,因为它被正常的参数分配算法过滤掉了。(另请注意,metaclass 是根据 PEP 3102 的关键字专用参数。)

即使 __prepare__ 不是必需的,默认元类 (‘type’) 也会实现它,以便子类通过 super() 调用它。

__prepare__ 返回一个类似字典的对象,该对象用于在类体求值期间存储类成员定义。换句话说,类体被评估为一个函数块(就像现在一样),除了局部变量字典被从 __prepare__ 返回的字典替换。此字典对象可以是常规字典或自定义映射类型。

此类似字典的对象不需要支持完整的字典接口。支持有限字典操作的字典将限制在类体求值期间可以执行的操作类型。最小实现可能只支持向字典中添加和检索值——大多数类体在求值期间只会执行这些操作。对于某些类,可能希望支持删除。许多元类将需要之后复制此字典,因此迭代或其他用于读取字典内容的方法也可能有用。

__prepare__ 方法最常被实现为类方法而不是实例方法,因为它是在创建元类实例(即类本身)之前调用的。

类体完成求值后,元类将被调用(作为可调用对象),使用类字典,这与当前的元类机制没有区别。

通常,元类将创建一个自定义字典——要么是 dict 的子类,要么是对它的包装器——它将包含在类体求值之前或期间设置的附加属性。然后在第二阶段,元类可以使用这些附加属性进一步自定义类。

一个例子是使用有关成员声明顺序的信息来创建 C 结构体的元类。元类将提供一个自定义字典,该字典只记录插入的顺序。这不需要是完整的 ‘ordered dict’ 实现,而只是一个 Python 列表,其中包含为每个插入追加的 (key,value) 对。

请注意,在这种情况下,元类需要处理重复键的可能性,但在大多数情况下这很简单。元类可以使用第一个声明、最后一个声明、以某种方式将它们组合起来,或者 simply throw an exception。如何处理这种情况由元类决定。

示例

以下是一个简单的元类示例,它创建所有类成员名称的列表,按声明顺序排列

# The custom dictionary
class member_table(dict):
    def __init__(self):
        self.member_names = []

    def __setitem__(self, key, value):
        # if the key is not already defined, add to the
        # list of keys.
        if key not in self:
            self.member_names.append(key)

        # Call superclass
        dict.__setitem__(self, key, value)

# The metaclass
class OrderedClass(type):

    # The prepare function
    @classmethod
    def __prepare__(metacls, name, bases): # No keywords in this case
        return member_table()

    # The metaclass invocation
    def __new__(cls, name, bases, classdict):
        # Note that we replace the classdict with a regular
        # dict before passing it to the superclass, so that we
        # don't continue to record member names after the class
        # has been created.
        result = type.__new__(cls, name, bases, dict(classdict))
        result.member_names = classdict.member_names
        return result

class MyClass(metaclass=OrderedClass):
    # method1 goes in array element 0
    def method1(self):
        pass

    # method2 goes in array element 1
    def method2(self):
        pass

示例实现

Guido van Rossum 创建了一个实现新功能的补丁:https://bugs.python.org/issue1681101

备选提案

Josiah Carlson 建议使用 ‘type’ 而不是 ‘metaclass’ 这个名称,理论上真正被指定的是类型的类型。虽然这在技术上是正确的,但从创建新类的程序员的角度来看,这也是令人困惑的。从应用程序程序员的角度来看,他们感兴趣的 ‘type’ 是他们正在编写的类;该类型的类型是元类。

在讨论中,有些人反对 ‘两阶段’ 创建过程,其中元类被调用两次,一次是创建类字典,一次是 ‘完成’ 类。有些人认为这两个阶段应该完全分开,也就是说应该有单独的语法来指定自定义字典,就像指定元类一样。但是,在大多数情况下,这两者会紧密相连,并且元类很可能非常了解类字典的内部细节。要求程序员确保使用正确的字典类型和正确的元类类型会给程序员带来额外的、不必要的负担。

另一个好建议是 simply 使用有序字典作为所有类,并跳过整个 ‘自定义字典’ 机制。这是基于这样的观察,即自定义字典的大多数用例是为了保留顺序信息。但是,这个想法有几个缺点,首先是因为这意味着必须将有序字典实现添加到 Python 的内置类型集中,其次是因为它会对所有类声明造成轻微的速度(和复杂性)损失。后来,几个人提出了使用自定义字典的其他用例(而不是保留字段顺序),因此这个想法被放弃了。

向后兼容性

可以保留现有的 __metaclass__ 语法。或者,修改 Py3K 翻译工具的语法规则以从旧语法转换为新语法并不困难。

参考资料

[1] [Python-3000] Py3K 中的元类(最初的提议) https://mail.python.org/pipermail/python-3000/2006-December/005030.html

[2] [Python-3000] Py3K 中的元类(Guido 建议的语法) https://mail.python.org/pipermail/python-3000/2006-December/005033.html

[3] [Python-3000] Py3K 中的元类(反对两阶段初始化) https://mail.python.org/pipermail/python-3000/2006-December/005108.html

[4] [Python-3000] Py3K 中的元类(始终使用有序字典) https://mail.python.org/pipermail/python-3000/2006-December/005118.html


来源:https://github.com/python/peps/blob/main/peps/pep-3115.rst

上次修改时间:GMT 2023 年 9 月 9 日 17:39:29