原文地址:https://blog.ionelmc.ro/2015/02/09/understanding-python-metaclasses/
现存的文章没有说明白metaclass是如何在Python中工作的,所以在这里我给出我的理解。metaclass在python中永远是一个争议的话题。许多开发者避免使用它们,而我认为这很大程度上是有任意的工作流程和查找规则引起的,它们没能很好的解释。同时,对于metaclass,总有几个你需要理解关键概念。
如果你没听说过metaclass,那么他们为减少样板和更好的API提供了很多有趣的机会。为了使metaclass尽可能简单,我将假设创造的类型将读取他。这意味着我们将跳过整个难题——为了A而不是B使用metaclass。
这篇文章使用Python3,如果中间有特殊的python2需要注意,将会在脚注用小字说明(脚注我没翻译,看原文吧)。
快速的概述
在我们深入细节之前,一个概括性的说明是必不可少的。
class是一个对象,跟其他普通的对象一样,是某个东西的实例:比如metaclass。默认的metaclass是type。不幸的是,对于向后的兼容性,type有点乱:他同样可以用作一个function来返回一个对象的class。
class Foobar:
pass
print(type(Foobar))
foo = Foobar()
print(type(foo))
返回值
<class 'type'>
<class '__main__.Foobar'>
如果你熟悉isinstance函数,你会知道这些:
print(isinstance(foo, Foobar))
# True
print(isinstance(Foobar, type))
# True
foo是Foobar的实例,Foobar是type的实例,也就是下图,class是metaclass的实例。
接着让我继续来建造class。
简单的metaclass使用
我们能使用type直接的创建class,并不需要其他的class声明:
MyClass = type('MyClass', (), {})
print(MyClass)
返回值
<class '__main__.MyClass'>
class type(name, bases, dict)
使用1个参数,返回对象的类型。就像object.__class__。内置函数isinstance()被用来测试对象的类型,因为他会考虑到子类。
用3个参数,返回一个新类型对象。本质上,这是类声明的一种动态形式。
参数name是一个字符串,表示类名称,并记录为__name__属性;
参数bases是一个元组,一个个记下基础类,并记录为__bases__属性,
参数dict是一个字典,包含类本体的命名空间并被赋值到标准字典。并记录为__dict__属性。
举个例子下面两个声明创建了相同类型的对象:
class X:
a = 1
X = type('X', (object,), dict(a=1))
在3.6版发生改变,type的子类不能重写type.__new__ ,不久将不再使用单参数的样式获取对象的类型。
class声明并不仅仅是语法糖,他还会做一些额外的事情,比如设置合适的__qualname__和__doc__属性或者调用__prepare__。
我们可以自定义一个metaclass,并使用它:
class Meta(type):
pass
class Complex(metaclass=Meta):
pass
class Complex2(Meta):
pass
print(type(Complex))
print(Complex)
print(type(Complex2))
print(type(type))
print(Complex2)
返回值
<class '__main__.Meta'>
<class '__main__.Complex'>
<class 'type'>
<class 'type'>
<class '__main__.Complex2'>
现在我们大概了解了我们将要处理的事情。。。
我这里加了一个Complex2()类,为了说明这里是设置metaclass而不是继承。
魔术方法
魔术方法是python的一个特点:他们允许程序员重写变量操作符号和对象的行为。调用者需要这样来重写:
class Funky:
def __call__(self):
print("Look at me, I work like a function!")
f = Funky()
f()
返回值就是print的那句话了。像function一样工作。
metaclass依赖一些魔术方法,所以多了解一些是非常有用的。
slots(定位,跟踪)
当你在class中定义一个魔术方法的时候,function除了__dict__中的条目之外,在整个类结构中,作为一个描述着这个class的指针一样结束。这个结构对于每一个魔术方法有一个字段。出于一些原因这些字段被称为type slots。
现在,这里有另一个特征,通过__slots__属性执行,一个拥有__slots__的class创造的实例不包含__dict__(这将使用更少的内存)。副作用是实例不能出现未在__slots__中指定的字段:如果你尝试设置一个不存在于__slots__中的字段,那么将会获得一个报错。
本文提及的单独的slots都是type slots不是__slots__。(类里的魔术方法)
class Foobar:
"""
A class that only allows these attributes: "a", "b" or "c"
"""
__slots__ = "a", "b", "c"
foo = Foobar()
foo.a = 1
# foo.x = 2
样例中去掉最后一行注释,foo.x = 2会报错。
对象属性查找
这里很容易出错,因为和python2的就样式相比有很多细小的不同。
假设我们有一个类和一个实例,并且实例是类的实例,获取(评估:原文用evaluate)实例的footbar大概相当于下面这样:
为Class.__getattribute__ (tp_getattro)调用type slot。默认会执行下面:
Class.__dict__是否有一个foobar元素是一个数据描述符?
如果有,返回Class.__dict__['foobar'].__get__(instance, Class)
instance.__dict__是否有一个foobar元素?
如果有,返回instance.__dict__['foobar']
Class.__dict__是否有一个foobar元素但并不是数据描述符?
如果有,返回Class.__dict__['foobar'].__get__(instance, klass)
Class.__dict__是否有一个foobar元素?
如果有,返回Class.__dict__['foobar']
如果属性还没找到,如果有Class.__getattr__,就会调用Class.__getattr__('foobar')
如果你还不清楚,请看下图:
为了避免点号'.'带来的混淆,图里用了冒号':'。
类属性查找
当你查找(评估:原文用evaluate)一些类似于class的foobar,由于class需要能够支持classmathod和staticmethod装饰器,所以和查找实例的foobar有一点不同。
假设类是metaclass的实例,查找(评估:原文用evaluate)class的foobar相当于下面这样:
为Metaclass.__getattribute__ (tp_getattro)调用type slot。默认会执行下面:
Metaclass.__dict__是否有一个foobar元素是一个数据描述符?
如果有,返回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']
如果属性还没找到,并且有Metaclass.__getattr__,就会调用Metaclass.__getattr__('foobar')
整个流程如下图:
魔术方法查看
对于魔术方法来说,查找已经完成了,直接在大结构上用slots。
对象的类是否有关于魔术方法的slot(大概就像c语言中object->ob_type->tp_<魔术方法>)?如果有,就使用,如果是NULL,那么选项不被支持。
在C中:
object->ob_type是对象的类
ob_type->tp_<魔术方法>是type slot
这看起来很简单,然而type slots在你function的外包装上到处都是,所以描述符就按照预期工作:
class Magic:
@property
def __repr__(self):
def inner():
return "It works!"
return inner
print(repr(Magic()))
这是否意味着这些地方并没有遵守规则,并且用不同的方式找到了slot?很遗憾是的,继续。。。
__new__方法
__new__方法是class和metaclass之间最容易混淆的方法之一。他有一些非常特别的约定。
当__init__只是一个初始化装置(当__init__被调用的时候,实例已经被创建了)的时候,__new__方法是一个创造者(因为他返回新的实例)。
假设有下面的class:
class Foobar:
def __new__(cls):
return super().__new__(cls)
现在你重新调用之前的部分,你将期待__new__将会在metaclass上查找,但是很遗憾,对于这种情况他并不是很有用,所以他查找的很安静。
当Foobar类希望这个魔术方法在他本身查找,而不是像其他的魔术方法在父级。这一点非常重要,因为class和metaclass都能定义方法:
Foobar.__new__被用来重建Foobar实例
type.__new__被用来创建Foobar类(例子中的type的实例)
__prepare__方法
这个方法被第要用在class本体被执行之前并且他必须返回一个类似字典的对象,这个对象被用来作为class本体的所有代码的本地命名空间。(在类中namespace参数可以取到__prepare__的返回值)在python3的时候加入。
如果你的__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__返回一个对象,这个对象只能执行__getitem__和__setitem__:
class DictLike:
def __init__(self):
self.data = {}
def __getitem__(self, name):
print('__getitem__(%r)' % name)
return self.data[name]
def __setitem__(self, name, value):
print('__setitem__(%r, %r)' % (name, value))
self.data[name] = value
class CustomNamespaceMeta(type):
def __prepare__(name, bases):
return DictLike()
然而,__new__将会抱怨:
class Foobar(metaclass=CustomNamespaceMeta):
a = 1
b = 2
__getitem__('__name__')
__setitem__('__module__', '__main__')
__setitem__('__qualname__', 'Foobar')
__setitem__('a', 1)
__setitem__('b', 2)
Traceback (most recent call last):
File "test.py", line 99, in <module>
class Foobar(metaclass=CustomNamespaceMeta):
TypeError: type.__new__() argument 3 must be dict, not DictLike
我们必须把它转化成真正的字典(或者他的一个子类):
class FixedCustomNamespaceMeta(CustomNamespaceMeta):
def __new__(mcs, name, bases, namespace):
return super().__new__(mcs, name, bases, namespace.data)
接着,一切跟我期待的一样:
class Foobar(metaclass=FixedCustomNamespaceMeta):
a = 1
b = 2
__getitem__('__name__')
__setitem__('__module__', '__main__')
__setitem__('__qualname__', 'Foobar')
__setitem__('a', 1)
__setitem__('b', 2)
下面这段代码我添了点东西,上面理解了你可以不看:
class DictLike:
def __init__(self):
self.data = {}
def __getitem__(self, name):
print('__getitem__(%r)' % name)
return self.data[name]
def __setitem__(self, name, value):
print('__setitem__(%r, %r)' % (name, value))
self.data[name] = value
class CustomNamespaceMeta(type):
def __prepare__(name, bases):
d = DictLike()
print(d)
print(d.__dict__)
return d
class FixedCustomNamespaceMeta(CustomNamespaceMeta):
def __new__(mcs, name, bases, namespace):
print(mcs)
print(name)
print(namespace)
print(namespace.__dict__)
return super().__new__(mcs, name, bases, namespace.data)
class Foobar(metaclass=FixedCustomNamespaceMeta):
a = 1
b = 2
返回值
<__main__.DictLike object at 0x04F53790>
{'data': {}}
__getitem__('__name__')
__setitem__('__module__', '__main__')
__setitem__('__qualname__', 'Foobar')
__setitem__('a', 1)
__setitem__('b', 2)
<class '__main__.FixedCustomNamespaceMeta'>
Foobar
<__main__.DictLike object at 0x04F53790>
{'data': {'__module__': '__main__', '__qualname__': 'Foobar', 'a': 1, 'b': 2}}
返回值中可以看出namespace和__prepare__的返回值是一个东西。
把他们放在一起
先介绍一下实例是如何构建的:
如何读这个泳道图:
水平的两块泳道代表你定义function的地方。
实心的线意味着function被调用了。
从Metaclass.__call__到Class.__new__的线意味着Metaclass.__call__将调用Class.__new__。
虚线意味着有一些东西要返回。
Class.__new__返回了一个Class的实例。
Metaclass.__call__返回了一切Class.__new__返回的东西(如果他返回了一个class实例,他也要在上面调用class.__init__)。
写数字红圆圈记录了调用顺序。
创造一个class也非常的相似:
简单的写下:
Metaclass.__prepare__只是返回命名空间对象(一个类似字典的对象,像之前解释的那样)。
Metaclass.__new__返回Class对象
Metaclass.__call__返回一切Metaclass__new__ 返回的(返回一个metaclass的实例,他同样在实例上调用了Metaclass.__init__)。
无论是metaclass还是class,如果__new__没有返回实例,那么就不会触发__init__
所以,你会发现metaclass允许你定制对象生命周期中几乎所有的部分。
metaclass都是callable(callable是任何可以被调用的东西)
如果你重新看一遍这篇文章,你会注意到可以通过Metaclass.__call__做一个实例。这意味着你能把任何callable作为metaclass。
class Foo(metaclass=print):
pass
print(Foo)
返回
Foo () {'__module__': '__main__', '__qualname__': 'Foo'}
None
如果你使用function作为metaclass,那么子类不能继承你的function的metaclass,但是type会返回function的返回值的类型。
def func(*arg,**kwargs):
return 'func'
class Foo(metaclass=func):
pass
print(Foo)
print(type(Foo))
func
<class 'str'>
子类继承metaclass
与类装饰器相比,一个有利点就是子类继承了metaclass。
This is a consequence of the fact that Metaclass(...) returns an object which usually has Metaclass as the __class__.
这是Metaclass(...)返回对象的结果,对象通常把Metaclass作为__class__。
管理多个metaclass
相对于相同的class允许有多个baseclass,每一个baseclass可能有不同的metaclass,但是有个转折:一切必须是线性的-继承树必须有单一的叶子。
举个例子,双叶子的情况是不被接受的:
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 "test.py", line 135, in <module>
class Foobar(Base1, Base2):
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
下面这个就可以(并且将用叶子作为metaclass)
class Meta(type):
pass
class SubMeta(Meta):
pass
class Base1(metaclass=Meta):
pass
class Base2(metaclass=SubMeta):
pass
class Foobar(Base1, Base2):
pass
print(type(Foobar))
<class '__main__.SubMeta'>
方法签名
(我第一次听到这个东西,他是指方法名称和参数以及参数类型)
这里还有缺少一些重要的细节,比如方法的签名。让我们来看下class和metaclass与那些重要的被执行材料。
注意额外的**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__在metaclass中被执行,因为他没多大用处——当__init__被调用的时候,class已经被创建好了。这相当于拥有类装饰器但又有点不同,当生成子类的时候,__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)
__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=[__module__, __qualname__, __new__, __init__, __str__, __classcell__], **{'extra': 1})
Meta.__init__(cls=<class '__main__.Class'>, name='Class', bases=(), attrs=[__module__, __qualname__, __new__, __init__, __str__, __classcell__], **{'extra': 1})
注意当生成类实例的时候,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>