原文地址:https://blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/
译文参考:https://blog.csdn.net/wwx890208/article/details/80644400
国外的一位程序员(Ionel)在网上看了很多讲述Python元类的文章,但觉得没有一篇能讲明白元类在Python中是如何工作的,所以 Ionel 写了这篇文章,给出了自己的理解。
Python中,元类是一个充满争议的话题,很多开发者都避免使用元类。我认为导致这种现象的主要原因是,Python学习过程中没有很好地理解清楚Python灵活的工作流和查找规则。您需要理解一些关键的概念,才能在Python中有效地使用元类。
如果您以前没听过元类,那么简单来说,元类提供了很多有趣的途径,来减少模版的使用,并提供更加友好的API。为了使本文尽可能精炼,我假设读者们都富有创造性,这样我就不需要花大量的篇幅去解释:“为什么A需要用元类来实现,而B不需要?”。
本文使用Python 3,如果有Python 2值得注意的地方,会在脚注中用小字说明(脚注没翻译,看原文吧)。
概述
在深入讨论细节之前,我们有必要先作一个概括性的说明。
在Python中,类也是对象。与普通对象一样,类也是某个东西的实例——那就是元类。默认的元类是 type
。不幸的是,为了能向后兼容, type
有点难以理解: type
可以作为一个函数使用,返回对象所属的类。
>>> class Foobar:
... pass
...
>>> type(Foobar)
<class 'type'>
>>> foo = Foobar()
>>> type(foo)
<class '__main__.Foobar'>
如果熟悉内置函数 isinstance
,您应该知道:
>>> isinstance(foo, Foobar)
True
>>> isinstance(Foobar, type)
True
上述关系如图所示:
让我们继续回到类的创建上来……
元类的简单使用
创建类时,我们可以完全不用 class
声明语句,而是用 type
来直接创建:
>>> MyClass = type('MyClass', (), {})
>>> MyClass
<class '__main__.MyClass'>
class
声明不仅仅是语法糖,它还做了很多额外的工作,比如设置恰当的 __qualname__
、 __doc__
属性,以及调用 __prepare__
方法等。
我们可以自定义一个元类:
>>> class Meta(type):
... pass
然后使用它:
>>> class Complex(metaclass=Meta):
... pass
>>> type(Complex)
<class '__main__.Meta'>
现在,我们对元类的使用有了大概的了解……
魔法方法
魔法方法是Python的特性之一,它允许开发人员重载各种操作符以及对象的行为。您可以这样做,来重载调用操作符:
>>> class Funky:
... def __call__(self):
... print("Look at me, I work like a function!")
>>> f = Funky()
>>> f()
Look at me, I work like a function!
元类的实现依赖于魔法方法,因此我们需要对这些魔法方法有所了解。
slots
当您在类中定义一个魔法方法时,此方法最终会成为描述该类的结构体中的一个指针,并放入 __dict__
。对每个魔法方法,这个结构体中都有对应的字段。出于一些原因,这些字段被称为 type slots 。
而Python通过 __slots__
属性实现了另外一个特性。带有 __slots__
属性的类,创建的实例是没有 __dict__
属性的(这样实例占的内存会少一丁点)。随之而来的副作用也很明显,除了 __slots__
中指定的属性,实例不能再拥有其他属性:如果尝试给实例设置一个不在 __slots__
中的属性,Python会抛出异常。
需要注意是,本文提到的 slots ,指的都是 type slots ,而不是 __slots__
。
对象属性查找
这里很容易出错,因为和Python 2的经典类相比,Python 3新式类的实现有了很多细微的差别。
这里假设 Class
是一个类, instance
是类 Class
的一个实例(为了和下一节区别,这里可理解为: instance
是一个实例对象),那么引用 instance.foobar
属性时的查找流程大致如下:
-
调用
Class.__getattribute__
(tp_getattro
)对应的 type slot 。此方法的默认实现如下:Class.__dict__
中是否存在名为foobar
的元素,此元素有__get__
方法,且是数据描述符?- 若存在,返回
Class.__dict__['foobar'].__get__(instance, Class)
的返回值。
- 若存在,返回
instance.__dict__
中是否存在名为foobar
的元素?- 若存在,返回
instance.__dict__['foobar']
。
- 若存在,返回
Class.__dict__
中是否存在名为foobar
的元素,此元素有__get__
方法,但不是数据描述符?- 若存在,返回
Class.__dict__['foobar'].__get__(instance, Class)
的返回值。
- 若存在,返回
Class.__dict__
中是否存在名为foobar
的元素?- 若存在,则返回
Class.__dict__['foobar']
。
- 若存在,则返回
-
若仍未找到
foobar
,但存在Class.__getattr__
,则调用Class.__getattr__('foobar')
并返回。
如果还不清楚,可参考下图,下图展示了对象属性查找的整个过程:
为了避免点号 “
.
” 造成混淆,图里使用冒号 “:
” 表示所处位置。
类属性查找
由于类需要支持类方法和静态方法,所以和引用对象属性( instance.foobar
)的查找流程相比,引用类属性( Class.foobar
)时的查找流程略有不同。
假设 Class
是 Metaclass
的一个实例(为了和上一节区别,这里可理解为: Class
是一个类对象),引用 Class.foobar
时的流程大致如下:
-
调用
Metaclass.__getattribute__
(tp_getattro
)对应的 type slot 。此方法的默认实现如下:Metaclass.__dict__
中是否存在名为foobar
的元素,此元素有__get__
方法,且是数据描述符?- 若存在,返回
Metaclass.__dict__['foobar'].__get__(Class, Metaclass)
的返回值。
- 若存在,返回
Class.__dict__
中是否存在名为foobar
的元素,且此元素是描述符(数据/非数据描述符)?- 若存在,返回
Class.__dict__['foobar'].__get__(None, Class)
的返回值。
- 若存在,返回
Class.__dict__
中是否存在名为foobar
的元素?- 若存在,返回
Class.__dict__['foobar']
。
- 若存在,返回
Metaclass.__dict__
中是否存在名为foobar
的元素,但此元素不是数据描述符?- 若存在,返回
Metaclass.__dict__['foobar'].__get__(Class, Metaclass)
的返回值。
- 若存在,返回
Metaclass.__dict__
中是否存在名为foobar
的元素?- 若存在,则返回
Metaclass.__dict__['foobar']
。
- 若存在,则返回
-
若仍未找到
foobar
属性,但存在Metaclass.__getattr__
,则调用Metaclass.__getattr__('foobar')
并返回。
整个流程如下图所示:
为了避免点号 “
.
” 造成混淆,图里使用冒号 “:
” 表示所处位置。
魔法方法查找
魔方方法的查找是在类上完成的,直接使用类的大结构体的 slots。
-
对象的类中是否存在此魔法方法对应的 slot (C代码大致如下:
object->ob_type->tp_<magicmethod>
)?- 若存在,则直接使用。
- 否则,对应的指针为
NULL
,表示不支持此魔法方法对应的操作。
内部C代码的含义:
object->ob_type
表示 object 对应的类。ob_type->tp_<magicmethod>
表示 type slot 。
上述流程看似简单,实际上 type slots 指向的是包装器,包装器包装您自定义的函数,因此描述符才能按照预期工作:
>>> class Magic:
... @property
... def __repr__(self):
... def inner():
... return "It works!"
... return inner
...
>>> repr(Magic())
'It works!'
这就是魔方方法的实现。那么,有没有不遵守上述流程,以不同的方式来查找 slot 的呢?很遗憾,答案是 Yes ,让我们继续……
__new__
方法
__new__
方法是类和元类之间最容易混淆的方法之一。__new__
方法有些非常特殊的约定。
__new__
方法是一种构造器(它返回新的实例),__init__
方法只是一个初始化程序(当 __init__
被调用时,实例已经创建好了)。
假设有个类,定义如下:
class Foobar:
def __new__(cls):
return super().__new__(cls)
如果您学习过上一节,会觉得应该在元类上查找 __new__
方法。不过遗憾的是,如果这样查找的话,__new__
的作用将大大减弱,而实际上,__new__
方法是静态查找的。
当 Foobar
类需要使用这个魔法方法时,它会在当前的对象(即 Foobar
类)上查找,而不像其他魔法方法,在其上一层查找。理解这一点非常重要,因为类和元类都可以定义 __new__
方法。
Foobar.__new__
被用来创建Foobar
的实例type.__new__
被用来创建Foobar
类(type
的一个实例)
__prepare__
方法
__prepare__
方法在 class
主体执行前调用,且必须返回一个类字典(dict-like)的对象,此对象被当作一个局部命名空间,这个命名空间包含了 class
主体的所有代码。此方法是Python 3.0新增的功能,详见PEP-3115。
如果您自定义的 __prepare__
返回一个对象 x
,那么下面这段代码:
class Class(metaclass=Meta):
a = 1
b = 2
c = 3
会这样修改 x
:
x['a'] = 1
x['b'] = 2
x['c'] = 3
这个 x
对象需要看起来像一个字典。需要注意的是,这个 x
对象最终会成为 Metaclass.__new__
方法的一个参数,并且如果它不是 dict
的实例,您还需要在调用 super().__new__
前手动转换它。
有趣的是,__prepare__
与 __new__
不同,并没有特殊的查找方式。似乎它没有属于自己的 type slot ,但如果您阅读过前面的章节,就会知道,__prepare__
的查找是通过类属性查找方式来实现的。
把它们放在一起
首先,如图所示,一个实例是如何被构建的:
如何理解这个泳道图呢?
- 水平区域是自定义的函数。
- 实线表示函数调用。
- 从
Metaclass.__call__
到Class.__new__
的实线,表示Metaclass.__call__
将要调用Class.__new__
。
- 从
- 虚线表示返回值。
Class.__new__
返回Class
的实例。Metaclass.__call__
返回Class.__new__
的返回值(如果Class.__new__
返回的是Class
的一个实例,Metaclass.__call__
还会在这个实例上调用Class.__init__
)。
- 红圆圈中的数字记录了调用顺序。
创建一个类的流程很类似:
有一些需要注意的地方:
Metaclass.__prepare__
只返回命名空间对象(如前文所述,是一个类字典 dict-like 对象)。Metaclass.__new__
返回Class
对象。MetaMetaclass.__call__
返回Metaclass.__new__
的返回值(如果Metaclass.__new__
返回的是Metaclass
的一个实例,MetaMetaclass.__call__
还会在这个实例上调用Metaclass.__init__
)。
可见,元类允许您自定义一个对象生命周期的几乎每个部分。
元类是可调用的
如果再仔细看一遍前两张图,您会注意到,创建实例是通过调用 Metaclass.__call__
实现的。这意味着,我们可以用任何可调用的东西作为元类。
>>> class Foo(metaclass=print): # pointless, but illustrative
... pass
...
Foo () {'__module__': '__main__', '__qualname__': 'Foo'}
>>> print(Foo)
None
如果您使用函数作为元类,那么子类将不能继承这个“函数元类”,而继承这个函数返回的类型。
子类继承元类
和类装饰器相比,元类的优势之一是:元类是可以被子类继承的。
这是因为 Metaclass(...)
返回了一个对象,这个对象的 __class__
是 Metaclass
。
多个元类的限制
Python允许一个类有多个父类,每个父类又可以有不同的元类。但幸运的是,一切都必须是线性的——即继承树只能有一片叶子。
例如下述代码,是非法的Python程序,因为存在2片叶子( Meta1
和 Meta2
):
>>> class Meta1(type):
... pass
...
>>> class Meta2(type):
... pass
...
>>> class Base1(metaclass=Meta1):
... pass
...
>>> class Base2(metaclass=Meta2):
... pass
...
>>> class Foobar(Base1, Base2):
... pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
这样才能正常工作(并且使用叶子做为元类):
>>> class Meta(type):
... pass
...
>>> class SubMeta(Meta):
... pass
...
>>> class Base1(metaclass=Meta):
... pass
...
>>> class Base2(metaclass=SubMeta):
... pass
...
>>> class Foobar(Base1, Base2):
... pass
...
>>> type(Foobar)
<class '__main__.SubMeta'>
方法签名
本文还缺少一些重要的细节,比如方法的签名。让我们通过类和元类来看看这些重要的实现。
注意 **kwargs
—— 用来收集其他不需要关注的关键字参数,这样就可以直接将它们传递给 class
声明语句。
>>> class Meta(type):
... @classmethod
... def __prepare__(mcs, name, bases, **kwargs):
... print(' Meta.__prepare__(mcs=%s, name=%r, bases=%s, **%s)' % (
... mcs, name, bases, kwargs
... ))
... return {}
之前提到过,__prepare__
方法返回的对象可能并不是 dict
的实例。因此,您必须确保在自定义的 __new__
方法中处理这种情况。
... def __new__(mcs, name, bases, attrs, **kwargs):
... print(' Meta.__new__(mcs=%s, name=%r, bases=%s, attrs=[%s], **%s)' % (
... mcs, name, bases, ', '.join(attrs), kwargs
... ))
... return super().__new__(mcs, name, bases, attrs)
在元类中自定义 __init__
并不常见,因为 __init__
的作用并不大——当 __init__
被调用时,类已经构建好了。元类的 __init__
作用大致相当于一个类装饰器,二者的区别在于,创建子类时,元类的 __init__
会运行,而子类并不会调用父类的类装饰器。
... def __init__(cls, name, bases, attrs, **kwargs):
... print(' Meta.__init__(cls=%s, name=%r, bases=%s, attrs=[%s], **%s)' % (
... cls, name, bases, ', '.join(attrs), kwargs
... ))
... return super().__init__(name, bases, attrs)
当您创建 Class
的实例时,__call__
方法会被调用。
... def __call__(cls, *args, **kwargs):
... print(' Meta.__call__(cls=%s, args=%s, kwargs=%s)' % (
... cls, args, kwargs
... ))
... return super().__call__(*args, **kwargs)
...
使用 Meta
(注意 extra=1
):
>>> class Class(metaclass=Meta, extra=1):
... def __new__(cls, myarg):
... print(' Class.__new__(cls=%s, myarg=%s)' % (
... cls, myarg
... ))
... return super().__new__(cls)
...
... def __init__(self, myarg):
... print(' Class.__init__(self=%s, myarg=%s)' % (
... self, myarg
... ))
... self.myarg = myarg
... return super().__init__()
...
... def __str__(self):
... return "<instance of Class; myargs=%s>" % (
... getattr(self, 'myarg', 'MISSING'),
... )
Meta.__prepare__(mcs=<class '__main__.Meta'>, name='Class', bases=(),
**{'extra': 1})
Meta.__new__(mcs=<class '__main__.Meta'>, name='Class', bases=(),
attrs=[__qualname__, __new__, __init__, __str__, __module__],
**{'extra': 1})
Meta.__init__(cls=<class '__main__.Class'>, name='Class', bases=(),
attrs=[__qualname__, __new__, __init__, __str__, __module__],
**{'extra': 1})
注意,当我们创建 Class
的实例时,Meta.__call__
会被调用:
>>> Class(1)
Meta.__call__(cls=<class '__main__.Class'>, args=(1,), kwargs={})
Class.__new__(cls=<class '__main__.Class'>, myarg=1)
Class.__init__(self=<instance of Class; myargs=MISSING>, myarg=1)
<instance of Class; myargs=1>
尾声
现在,我已经把一切都写了出来,似乎写了很多,比我想象的还要多。但我觉得还是遗漏了什么重要的细节 😃
下一篇文章,我们将讨论这些元类理论的实际应用……