关于Python2.x中metaclass这一黑科技,我原以为我是懂的,只有当被打脸的时候,我才认识到自己too young too simple sometimes native。

  为什么之前我认为自己懂了呢,因为我阅读过stackoverflow上的《what-is-a-metaclass-in-python》这一神作(注意,本文中专指e-satis的回答),在伯乐在线上也有不错的翻译《深刻理解Python中的元类(metaclass)》。而且在实际项目中也使用过metaclass,比如creating-a-singleton-in-python一文中提到的用metaclass创建单例,比如用metaclass实现mixin效果,当然,正是后面这个使用案列让我重新认识metaclass。

  本文地址:http://www.cnblogs.com/xybaby/p/7927407.html

要点回顾

  不得不承认《what-is-a-metaclass-in-python》真的是非常棒,仔细阅读完这篇文章,基本上就搞清了metaclass。因此在这里,只是强调一些要点,强烈建议还没阅读过原文的pythoner去阅读一下。

第一:everything is object

  python中,一切都是对象,比如一个数字、一个字符串、一个函数。对象是类(class)的是实例,类(class)也是对象,是type的实例。type对象本身又是type类的实例(鸡生蛋还是蛋生鸡?),因此我们称type为metaclass(中文元类)。在《python源码剖析》中,有清晰的表示

  在python中,可以通过对象的__class__属性来查看对应的类,也可以通过isinstance来判断一个对象是不是某一个类的实例。for example:

>>> class OBJ(object):

... a = 1
...
>>> o = OBJ()
>>> o.__class__
<class '__main__.OBJ'>
>>> isinstance(o, OBJ)
True
>>> OBJ.__class__
<type 'type'>
>>> isinstance(OBJ, type)
True
>>> type.__class__
<type 'type'>
>>>

第二:metaclass可以定制类的创建

  我们都是通过class OBJ(obejct):pass的方式来创建一个类,上面有提到,类(class)是type类型的实例,按照我们常见的创建类的实例(instance)的方法,那么类(class)应该就是用 class="type"(*args)的方式创建的。确实如此,python document中有明确描述:

class type(name, bases, dict)
With three arguments, return a new type object. This is essentially a dynamic form of the class statement. The name string is the class name and becomes the __name__ attribute; the bases tuple itemizes the base classes and becomes the __bases__ attribute; and the dict dictionary is the namespace containing definitions for class body and becomes the __dict__ attribute. For example, the following two statements create identical type objects:

  该函数返回的就是一个class,三个参数分别是类名、基类列表、类的属性。比如在上面提到的OBJ类,完全等价于:OBJ = type('OBJ', (), {'a': 1})

  当然,用上面的方式创建一个类(class)看起来很傻,不过其好处在于可以动态的创建一个类。

  python将定制类开放给了开发者,type也是一个类型,那么自然可以被继承,type的子类替代了Python默认的创建类(class)的行为,什么时候需要做呢

  Some ideas that have been explored including logging, interface checking, automatic delegation, automatic property creation, proxies, frameworks, and automatic resource locking/synchronization.

  那么当我们用class OBJ(obejct):pass的形式声明一个类的时候,怎么指定OBJ的创建行为呢,那就是在类中使用__metaclass__。最简单的例子:

 class Metaclass(type):
def __new__(cls, name, bases, dct):
print 'HAHAHA'
dct['a'] = 1
return type.__new__(cls, name, bases, dct) print 'before Create OBJ'
class OBJ(object):
__metaclass__ = Metaclass
print 'after Create OBJ' if __name__ == '__main__':
print OBJ.a

  运行结果:

before Create OBJ
HAHAHA
after Create OBJ

  可以看到在代码执行的时候,在创建OBJ这个类的时候,__metaclass__起了作用,为OBJ增加了一个类属性‘a'

第三:关于__metaclass__的两个细节

  首先,__metaclass__是一个callable即可,不一定非得是一个类,在what-is-a-metaclass-in-python就有__metaclass__是function的实例,也解释了为什么__metaclass__为一个类是更好的选择。

  其次,就是如何查找并应用__metaclass__,这哥在what-is-a-metaclass-in-python没用详细介绍,但是在python document中是有的:

The appropriate metaclass is determined by the following precedence rules:
  ● If dict['__metaclass__'] exists, it is used.
  ● Otherwise, if there is at least one base class, its metaclass is used (this looks for a __class__ attribute first and if not found, uses its type).
  ● Otherwise, if a global variable named __metaclass__ exists, it is used.
  ● Otherwise, the old-style, classic metaclass (types.ClassType) is used.

  即:

    先从类的dict中查找,否则从基类的dict查找(这里会有一些需要注意的细节,后文会提到),否则从global作用域查找,否则使用默认的创建方式

  对应的python源码在ceval.c::build_class,核心代码如下,很明了。

 static PyObject *
build_class(PyObject *methods, PyObject *bases, PyObject *name)
{
PyObject *metaclass = NULL, *result, *base; if (PyDict_Check(methods))
metaclass = PyDict_GetItemString(methods, "__metaclass__");
if (metaclass != NULL)
Py_INCREF(metaclass);
else if (PyTuple_Check(bases) && PyTuple_GET_SIZE(bases) > 0) {
base = PyTuple_GET_ITEM(bases, 0);
metaclass = PyObject_GetAttrString(base, "__class__");
if (metaclass == NULL) {
PyErr_Clear();
metaclass = (PyObject *)base->ob_type;
Py_INCREF(metaclass);
}
}
else {
PyObject *g = PyEval_GetGlobals();
if (g != NULL && PyDict_Check(g))
metaclass = PyDict_GetItemString(g, "__metaclass__");
if (metaclass == NULL)
metaclass = (PyObject *) &PyClass_Type;
Py_INCREF(metaclass);
}
result = PyObject_CallFunctionObjArgs(metaclass, name, bases, methods,
NULL);
Py_DECREF(metaclass);
if (result == NULL && PyErr_ExceptionMatches(PyExc_TypeError)) {
/* A type error here likely means that the user passed
in a base that was not a class (such the random module
instead of the random.random type). Help them out with
by augmenting the error message with more information.*/ PyObject *ptype, *pvalue, *ptraceback; PyErr_Fetch(&ptype, &pvalue, &ptraceback);
if (PyString_Check(pvalue)) {
PyObject *newmsg;
newmsg = PyString_FromFormat(
"Error when calling the metaclass bases\n"
" %s",
PyString_AS_STRING(pvalue));
if (newmsg != NULL) {
Py_DECREF(pvalue);
pvalue = newmsg;
}
}
PyErr_Restore(ptype, pvalue, ptraceback);
}
return result;
}

ceval::build_class

我遇到的问题

  在项目中,我们使用了metaclass来实现Mixin的行为,即某一个类拥有定义在其他一些类中的行为,简单来说,就是要把其他类的函数都注入到这个类。但是我们不想用继承的方法,一来,语义上不是is a的关系,不使用继承;二来,python的mro也不是很东西。我们是这么干的,伪码如下:

 import inspect
import types
class RunImp(object):
def run(self):
print 'just run' class FlyImp(object):
def fly(self):
print 'just fly' class MetaMixin(type):
def __init__(cls, name, bases, dic):
super(MetaMixin, cls).__init__(name, bases, dic)
member_list = (RunImp, FlyImp) for imp_member in member_list:
if not imp_member:
continue for method_name, fun in inspect.getmembers(imp_member, inspect.ismethod):
setattr(cls, method_name, fun.im_func) class Bird(object):
__metaclass__ = MetaMixin print Bird.__dict__
print Bird.__base__

  运行结果如下:

{'fly': <function fly at 0x025220F0>, '__module__': '__main__', '__metaclass__': <class '__main__.MetaMixin'>, '__dict__': <attribute '__dict__' of 'Bird' objects>, 'run': <function run at 0x025220B0>, '__weakref__': <attribute '__weakref__' of 'Bird' objects>, '__doc__': None}
<type 'object'>

  可以看到,通过metaclass,Bird拥有了run fly两个method。但是类的继承体系没有收到影响。

重载通过MetaMixin中注入的方法

  某一日需求变化,需要继承自Brid,定义特殊的Bird,重载run方法,新增代码如下;

 class Bird(object):
__metaclass__ = MetaMixin class SpecialBird(Bird):
def run(self):
print 'SpecialBird run' if __name__ == '__main__':
b = SpecialBird()
b.run()

  运行结果:

just run

  what?!,重载根本不生效。这似乎颠覆了我的认知:Bird类有一个run属性,子类SpecialBird重载了这个方法,那么就应该调用子类的方法啊。

  什么原因呢,答案就在上面提到的__metaclass__查找顺序,因为SpecialBird自身没有定义__metaclass__属性,那么会使用基类Bird的__metaclass__属性,因此虽然在SpecialBird中定义了run方法,但是会被MetaMixin给覆盖掉。使用dis验证如下

 import dis

 class SpecialBird(Bird):
def run(self):
print 'SpecialBird run'
dis.dis(run)
dis.dis(SpecialBird.run)

  

  可以看到在SpecialBird.run方法本来是类中显示定义的方法,后来被MetaMixin所覆盖了。

防止属性被意外覆盖

  这就暴露出了一个问题,当前版本的MetaMixin可能导致属性的覆盖问题。比如在RunImp、FlyImp有同名的函数foo时,在创建好的Bird类中,其foo方法来自于FlyImp,而不是RunImp。通用,即使在Bird内部也定义foo方法,也会被FlyImp.foo覆盖。

  这显然不是我们所期望的结果,这也是python的陷阱没有报错,但是以错误的方式运行。我们要做的就是尽早把这个错误爆出来。实现很简单,只需要简单修改MetaMixin,见高亮标示。

 class MetaMixin(type):
def __init__(cls, name, bases, dic):
super(MetaMixin, cls).__init__(name, bases, dic)
member_list = (RunImp, FlyImp) for imp_member in member_list:
if not imp_member:
continue for method_name, fun in inspect.getmembers(imp_member, inspect.ismethod):
assert not hasattr(cls, method_name), method_name
setattr(cls, method_name, fun.im_func)

  当我们修改MetaMixin之后,再次运行下面的代码的时候就报错了

class RunImp(object):
def run(self):
print 'just run' class FlyImp(object):
def fly(self):
print 'just fly' class MetaMixin(type):
def __init__(cls, name, bases, dic):
super(MetaMixin, cls).__init__(name, bases, dic)
member_list = (RunImp, FlyImp) for imp_member in member_list:
if not imp_member:
continue for method_name, fun in inspect.getmembers(imp_member, inspect.ismethod):
assert not hasattr(cls, method_name), method_name
setattr(cls, method_name, fun.im_func) class Bird(object):
__metaclass__ = MetaMixin class SpecialBird(Bird):
pass

会报错的完整代码

  运行结果抛了异常

Traceback (most recent call last):
assert not hasattr(cls, method_name), method_name
AssertionError: run

  呃,代码总共就几行,只有一个run方法啊,怎么会报错说有重复的方法呢,在MetaMixin中加一点log

 class RunImp(object):
def run(self):
print 'just run' class FlyImp(object):
def fly(self):
print 'just fly' class MetaMixin(type):
def __init__(cls, name, bases, dic):
super(MetaMixin, cls).__init__(name, bases, dic)
member_list = (RunImp, FlyImp) for imp_member in member_list:
if not imp_member:
continue for method_name, fun in inspect.getmembers(imp_member, inspect.ismethod):
print('class %s get method %s from %s' % (name, method_name, imp_member))
# assert not hasattr(cls, method_name), method_name
setattr(cls, method_name, fun.im_func) class Bird(object):
__metaclass__ = MetaMixin class SpecialBird(Bird):
pass

加了log且不报错的代码

  运行结果:

class Bird get method run from <class '__main__.RunImp'>
class Bird get method fly from <class '__main__.FlyImp'>
class SpecialBird get method run from <class '__main__.RunImp'>
class SpecialBird get method fly from <class '__main__.FlyImp'>

  一目了然,原来在创建Bird的时候已经将run、fly方法注入到了bird.__dict__, SpecialBird继承子Bird,那么在Speialbird使用__metaclass__定制化之前,SpecialBird已经有了run、fly属性,然后再度运用metaclass的时候就检查失败了。

  简而言之,这个是一个很隐蔽的陷阱:如果基类定义了__metaclass__,那么子类在创建的时候会再次调用metaclass,然而理论上来说可能是没有必要的,甚至会有副作用

解决重复使用metaclass

  首先,既然我们知道首先在子类的dict中查找__metaclass__,找不到再考虑基类,那么我们子类(SpecialBird)中重新生命一个__metaclass__就好了,如下所示:

 class DummyMetaIMixin(type):
pass class SpecialBird(Bird):
__metaclass__ = DummyMetaIMixin

  很遗憾,抛出了一个我之前从未见过的异常

TypeError: Error when calling the metaclass bases
  metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

  意思很明显,子类的__metaclass__必须继承自基类的__metaclass__,那么再改改

 class DummyMetaIMixin(MetaMixin):
def __init__(cls, name, bases, dic):
type.__init__(cls, name, bases, dic) class SpecialBird(Bird):
__metaclass__ = DummyMetaIMixin

  This‘s OK!完整代码如下:

 class RunImp(object):
def run(self):
print 'just run' class FlyImp(object):
def fly(self):
print 'just fly' class MetaMixin(type):
def __init__(cls, name, bases, dic):
super(MetaMixin, cls).__init__(name, bases, dic)
member_list = (RunImp, FlyImp) for imp_member in member_list:
if not imp_member:
continue for method_name, fun in inspect.getmembers(imp_member, inspect.ismethod):
print('class %s get method %s from %s' % (name, method_name, imp_member))
assert not hasattr(cls, method_name), method_name
setattr(cls, method_name, fun.im_func) class Bird(object):
__metaclass__ = MetaMixin class DummyMetaIMixin(MetaMixin):
def __init__(cls, name, bases, dic):
type.__init__(cls, name, bases, dic) class SpecialBird(Bird):
__metaclass__ = DummyMetaIMixin

解决了子类重新使用metaclass的问题

metaclass __new__ __init__

  行文至此,使用过metaclass的pythoner可能会有疑问,因为网上的很多case都是在metaclass中重载type的__new__方法,而不是__init__。实时上,对于我们使用了MetaMixin,也可以通过重载__new__方法实现,而且还有意外的惊喜!

 class RunImp(object):
def run(self):
print 'just run' class FlyImp(object):
def fly(self):
print 'just fly' class MetaMixinEx(type):
def __new__(cls, name, bases, dic):
member_list = (RunImp, FlyImp) for imp_member in member_list:
if not imp_member:
continue for method_name, fun in inspect.getmembers(imp_member, inspect.ismethod):
print('class %s get method %s from %s' % (name, method_name, imp_member))
assert method_name not in dic, (imp_member, method_name)
dic[method_name] = fun.im_func
return type.__new__(cls, name, bases, dic) class Bird(object):
__metaclass__ = MetaMixinEx class SpecialBird(Bird):
pass

  运行结果

class Bird get method run from <class '__main__.RunImp'>
class Bird get method fly from <class '__main__.FlyImp'>
class SpecialBird get method run from <class '__main__.RunImp'>
class SpecialBird get method fly from <class '__main__.FlyImp'>

  从结果可以看到,虽然子类也重复运行了一遍metaclass, 但并没有报错!注意代码第18行是有assert的!为什么呢,本质是因为__new__和__init__两个magic method的区别

  绝大多数Python程序员都写过__init__方法,但很少有人写__new__方法,因为绝大多数时候,我们都无需重载__new__方法。python document也说了,哪些场景需要重载__new__方法呢

__new__() is intended mainly to allow subclasses of immutable types (like int, str, or tuple) to customize instance creation. It is also commonly overridden in custom metaclasses in order to customize class creation.

  即用于继承不可变对象,或者使用在metaclass中!

  那么__new__和__init__有什么却别呢

__new__:
  Called to create a new instance of class cls
__init__:
  Called when the instance is created.

  即__new__用于如何创建实例,而__init__是在实例已经创建好之后调用 

  注意,仅仅当__new__返回cls的实例时,才会调用__init__方法,__init__方法的参数同__new__方法。看下面的例子

 class OBJ(object):
def __new__(self, a): ins = object.__new__(OBJ, a)
print "call OBJ new with parameter %s, created inst %s" % (a, ins)
return ins # 去掉这行就不会再调用__init__ def __init__(self, a):
print "call OBJ new with parameter %s, inst %s" % (a, self) if __name__ == '__main__':
OBJ(123)

call OBJ new with parameter 123, created inst <__main__.OBJ object at 0x024C2470>
call OBJ new with parameter 123, inst <__main__.OBJ object at 0x024C2470>

  可以看到,__init__中的self正是__new__中创建并返回的ins,正如第6行的注释所示,如果去掉第6行(即不返回ins), 那么是不会调用__init__方法的。

  metaclass继承自type,那么其__new__、__init__和普通class的__new__、__init__是一样的,只不过metaclass的__new__返回的是一个类。我们看看metaclass的例子

 class Meta(type):
def __new__(cls, name, bases, dic):
print 'here class is %s' % cls
print 'class %s will be create with bases class %s and attrs %s' % (name, bases, dic.keys())
dic['what'] = name
return type.__new__(cls, name, bases, dic) def __init__(cls, name, bases, dic):
print 'here class is %s' % cls
print 'class %s will be inited with bases class %s and attrs %s' % (name, bases, dic.keys())
print cls.what
super(Meta, cls).__init__(name, bases, dic) class OBJ(object):
__metaclass__ = Meta
attr = 1 print('-----------------------------------------------')
class SubObj(OBJ):
pass

  输出结果:

here class is <class '__main__.Meta'>

class OBJ will be create with bases class (<type 'object'>,) and attrs ['__module__', '__metaclass__', 'attr']
here class is <class '__main__.OBJ'>
class OBJ will be inited with bases class (<type 'object'>,) and attrs ['__module__', '__metaclass__', 'attr', 'what']
OBJ
-----------------------------------------------
here class is <class '__main__.Meta'>
class SubObj will be create with bases class (<class '__main__.OBJ'>,) and attrs ['__module__']
here class is <class '__main__.SubObj'>
class SubObj will be inited with bases class (<class '__main__.OBJ'>,) and attrs ['__module__', 'what']
SubObj

  注意分割线。

  首先要注意虽然在new init方法的第一个参数都是cls,但是完全是两回事!

  然后在调用new之后,产生的类对象(cls如OBJ)就已经有了动态添加的what 属性

  在调用__new__的时候,dic只来自类的scope内所定义的属性,所以在创建SubObj的时候,dic里面是没有属性的,attr在基类OBJ的dict里面,也能看出在__new__中修改后的dic被传入到__init__方法当中。

references

what-is-a-metaclass-in-python

深刻理解Python中的元类(metaclass)

customizing-class-creation

special-method-names

关于metaclass,我原以为我是懂的的更多相关文章

  1. Javascript闭包——懂不懂由你,反正我是懂了

    摘要:“如果你不能向一个六岁的孩子解释清楚,那么其实你自己根本就没弄懂.”好吧,我试着向一个27岁的朋友就是JS闭包(JavaScript closure)却彻底失败了. 越来越觉得国内没有教书育人的 ...

  2. JavaScript闭包 懂不懂由你反正我是懂了

    原文链接:http://www.jb51.net/article/28611.htm 越来越觉得国内没有教书育人的氛围,为了弄懂JS的闭包,我使出了我英语四级吃奶的劲去google上搜寻着有关闭包的解 ...

  3. closure

    什么是闭包?百度的答案: 闭包是指可以包含自由(未绑定到特定对象)变量的代码块:这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)."闭包&quo ...

  4. JavaScript闭包(二)——作用

    一.延迟调用 当在一段代码中使用 setTimeout 时,要将一个函数的引用作为它的第一个参数,而将以毫秒表示的时间值作为第二个参数. 但是,传递函数引用的同时无法为计划执行的函数提供参数.可以在代 ...

  5. JavaScript闭包(一)——实现

    闭包的官方的解释是:一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. 通俗点的说法是: 从理论角度:所有的函数.因为它们都在创建的时候就将上层上下文 ...

  6. idea 到myeclipse

    在上一篇博客使用maven进行开发过程管理之准备篇中提到了maven的基本概念.IT男罗书全觉得概念我是懂了,但是那些东西似乎离我很远啊.先开发再说吧, 于是IT男罗书全就在svn上取了源代码,并开始 ...

  7. JavaScript闭包——实现

    闭包的官方的解释是:一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. 通俗点的说法是: 从理论角度:所有的函数.因为它们都在创建的时候就将上层上下文 ...

  8. map & flatMap 浅析

    我之前一直以为我是懂 map 和 flatMap 的.但是直到我看到别人说:「一个实现了 flatMap 方法的类型其实就是 monad.」我又发现这个熟悉的东西变得陌生起来,本节烧脑体操打算更细致一 ...

  9. TempData知多少

    网上对TempData的总结为: 保存在session中,Controller每次执行请求时,会从session中一次获取所有tempdata数据,保存在单独的内部数据字典中,而后从session中清 ...

随机推荐

  1. BZOJ 1257: [CQOI2007]余数之和sum【神奇的做法,思维题】

    1257: [CQOI2007]余数之和sum Time Limit: 5 Sec  Memory Limit: 162 MBSubmit: 4474  Solved: 2083[Submit][St ...

  2. 二维字符数组利用gets输入

    char a[10][81];for(int i=0;i<10;i++)gets(a[i]); a是二维数组的数组名,相当于一维数组的指针,所以a[i]就相当于指向第i个数组的指针,类型就相当于 ...

  3. D触发器深入详细介绍(zhuanzai)

    D触发器深入详细介绍,D触发器是对输入时钟脉冲边沿信号敏感的装置.只有在检测到边沿信号,才设置输出信号与输入端D相同.一个基础的电平触发装置是门控D锁存器. D触发器(英文中“D”代表“Data”,“ ...

  4. node中定时器, process.nextTick(), setImediate()的区别与联系

    1.定时器 setTimeout()和setInterval()与浏览器中的API是一致的,定时器的问题在于,他并非精确的(在容忍范围内).尽管事件循环十分快,但是如果某一次循环占用的时间较多,那么下 ...

  5. windows server 2008使用nginx转发API异常解决办法

    公司比较传统,一直使用的JSP做项目,没有遇到过跨域问题. 最近因为公司接到一个微信spa项目,因为考虑到项目需要调用老接口,斗胆选择nginx(1.12.1)做接口转发服务, 开发环境使用的win1 ...

  6. Linux命令之远程下载命令:wget

    转自:http://www.cnblogs.com/peida/archive/2013/03/18/2965369.html Linux系统中的wget是一个下载文件的工具,它用在命令行下.对于Li ...

  7. Win7如何分享局域网并设置共享文件夹账户和密码

    https://jingyan.baidu.com/article/ceb9fb10ddf6c08cad2ba017.html 在办公或者其他场所,我们需要分享自己的文件给朋友或者同事,但又不想同一局 ...

  8. asp.net -mvc框架复习(1)-ASP.NET网站开发概述

    1.网站开发的基本步骤: 2.网站开发的需要的知识结构 (1)网站开发前台页面技术 页面设计:HTML  .CSS+DIV 页面特效:JavaScript.jQery (2)OOP编程核心公共技能 C ...

  9. JS事件捕获和事件冒泡

    p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; line-height: 19.0px; font: 14.0px "Helvetica Neue" ...

  10. linux libpcap的性能问题,请大家注意绕行。

    内核代码中,ip_rcv是ip层收包的主入口函数,该函数由软中断调用.存放数据包的sk_buff结构包含有目的地ip和端口信息,此时ip层进行检查,如果目的地ip不是本机,且没有开启转发的话,则将包丢 ...