理解python metaclass

原文地址: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>

猜你喜欢

转载自blog.csdn.net/wwx890208/article/details/80644400