python 面向对象专题(十三):元类(二): metaclass魔术方法

0 魔术方法

魔术方法是python的一个特点:他们允许程序员重写变量操作符号和对象的行为。调用者需要这样来重写:

class Funky:
    def __call__(self):
        print("Look at me, I work like a function!")
f = Funky()
f()

返回值就是print的那句话了。像function一样工作。

metaclass依赖一些魔术方法,所以多了解一些是非常有用的。

1 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

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')

如果你还不清楚,请看下图:

python 面向对象专题(十三):元类(二): metaclass魔术方法

 为了避免点号'.'带来的混淆,图里用了冒号':'。

类属性查找

当你查找(评估:原文用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')

python 面向对象专题(十三):元类(二): metaclass魔术方法

魔术方法查看

对于魔术方法来说,查找已经完成了,直接在大结构上用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上查找,但是很遗憾,对于这种情况他并不是很有用,所以他查找的很安静。

__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__的返回值是一个东西。

把他们放在一起

先介绍一下实例是如何构建的:

python 面向对象专题(十三):元类(二): metaclass魔术方法

如何读这个泳道图:

水平的两块泳道代表你定义function的地方。

实心的线意味着function被调用了。

从Metaclass.__call__到Class.__new__的线意味着Metaclass.__call__将调用Class.__new__。

虚线意味着有一些东西要返回。

Class.__new__返回了一个Class的实例。

Metaclass.__call__返回了一切Class.__new__返回的东西(如果他返回了一个class实例,他也要在上面调用class.__init__)。

写数字红圆圈记录了调用顺序。

创造一个class也非常的相似:

python 面向对象专题(十三):元类(二): metaclass魔术方法

简单的写下:

Metaclass.__prepare__只是返回命名空间对象(一个类似字典的对象,像之前解释的那样)。

Metaclass.__new__返回Class对象

Metaclass.__call__返回一切Metaclass__new__ 返回的(返回一个metaclass的实例,他同样在实例上调用了Metaclass.__init__)。

无论是metaclass还是class,如果__new__没有返回实例,那么就不会触发__init__

所以,你会发现metaclass允许你定制对象生命周期中几乎所有的部分。