魔术方法是python的一个特点:他们允许程序员重写变量操作符号和对象的行为。调用者需要这样来重写:
class Funky: def __call__(self): print("Look at me, I work like a function!") f = Funky() f()
返回值就是print的那句话了。像function一样工作。
metaclass依赖一些魔术方法,所以多了解一些是非常有用的。
当你在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
这里很容易出错,因为和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__方法是class和metaclass之间最容易混淆的方法之一。他有一些非常特别的约定。
当__init__只是一个初始化装置(当__init__被调用的时候,实例已经被创建了)的时候,__new__方法是一个创造者(因为他返回新的实例)。
假设有下面的class:
class Foobar: def __new__(cls): return super().__new__(cls)
现在你重新调用之前的部分,你将期待__new__将会在metaclass上查找,但是很遗憾,对于这种情况他并不是很有用,所以他查找的很安静。
这个方法被第要用在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允许你定制对象生命周期中几乎所有的部分。