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中,因为它被正常的参数赋值算法过滤掉了。(另请注意,根据PEP 3102,metaclass是一个仅限关键字的参数。)
尽管不要求__prepare__
,但默认元类('type')实现了它,以方便子类通过super()调用它。
__prepare__
返回一个类似字典的对象,用于在评估类体期间存储类成员定义。换句话说,类体被评估为一个函数块(就像现在一样),只是局部变量字典被__prepare__
返回的字典替换了。这个字典对象可以是常规字典或自定义映射类型。
这个类似字典的对象不要求支持完整的字典接口。一个支持有限字典操作集的字典将限制在评估类体期间可能发生的动作类型。一个最小的实现可能只支持从字典中添加和检索值——大多数类体在评估期间不会做更多的事情。对于某些类,可能需要支持删除。许多元类之后需要复制这个字典,因此迭代或读取字典内容的其他方法也可能有用。
__prepare__
方法通常将作为类方法而不是实例方法实现,因为它在元类实例(即类本身)创建之前被调用。
一旦类体评估完成,元类将以类字典作为可调用对象被调用,这与当前的元类机制没有区别。
通常,元类会创建一个自定义字典——无论是dict的子类,还是它的包装器——其中包含在类体评估之前或期间设置的附加属性。然后在第二阶段,元类可以使用这些附加属性进一步自定义类。
一个例子是元类,它使用有关成员声明顺序的信息来创建C结构体。元类将提供一个自定义字典,该字典简单地记录插入的顺序。这不需要是一个完整的“有序字典”实现,而只是一个Python列表,其中包含(键,值)对,每次插入都会添加到其中。
请注意,在这种情况下,元类需要处理重复键的可能性,但在大多数情况下,这很简单。元类可以使用第一个声明、最后一个声明、以某种方式组合它们,或者直接抛出异常。由元类决定如何处理这种情况。
示例
这是一个简单的元类示例,它按照声明的顺序创建一个所有类成员名称的列表
# 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”,理由是实际指定的是类型的类型。虽然这在技术上是正确的,但从创建新类的程序员的角度来看,它也令人困惑。从应用程序程序员的角度来看,他们感兴趣的“类型”是他们正在编写的类;该类型的类型是元类。
在讨论中,有人反对“两阶段”创建过程,即元类被调用两次,一次创建类字典,一次“完成”类。一些人认为这两个阶段应该完全分开,即应该有单独的语法来指定自定义字典和指定元类。然而,在大多数情况下,两者将紧密结合在一起,元类很可能对类字典的内部细节有深入的了解。要求程序员确保正确使用字典类型和正确的元类类型会给程序员带来额外的、不必要的负担。
另一个好的建议是简单地对所有类使用有序字典,并跳过整个“自定义字典”机制。这是基于观察,即大多数自定义字典的用例是为了保留顺序信息。然而,这个想法有几个缺点,首先因为它意味着必须将有序字典的实现添加到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