资讯专栏INFORMATION COLUMN

Python: metaclass小记

mushang / 1413人阅读

摘要:最前面那个,解释器实际的流程是解析这段代码,得知它需要创建一个类对象,这个类的名字叫做它的父类列表用表示是,它的属性用一个来表示就是。解决方法很简单关键就是前面被特别标记了的应当返回这个的父类的方法返回的对象。

(原发于我的blog:Python: metaclass小记 )

友情提示:本文不一定适合阅读,如果执意要读,请备好晕车药。

题记

"Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don"t."

-- Tim Peters

起因

这句话听起来就很诱人,曾经试图去理解它,但是因为没有实际的需求,就因为烧脑子而放弃了。不妨摘录一段Python document里关于metaclass的概述,简直就是绕口令:

Terminology-wise, a metaclass is simply "the class of a class". Any class whose instances are themselves classes, is a metaclass. When we talk about an instance that"s not a class, the instance"s metaclass is the class of its class: by definition, x"s metaclass is x.__class__.__class__. But when we talk about a class C, we often refer to its metaclass when we mean C.__class__ (not C.__class__.__class__, which would be a meta-metaclass; there"s not much use for those although we don"t rule them out).

昨天心血来潮想写一个带class initializer的class,发现绕不过metaclass了,于是又翻出来看。

概述

其实是要理解metaclass的本质,无非是要时刻牢记两点:1. Python中一切皆对象; 2. class也是一个对象,它的class就是metaclass。

举例来说:

class A(object): pass
a = A()
print (a, id(a), type(a))

(<__main__.A object at 0xb183d0>, 11633616, )

print (A, id(A), type(A))

(, 11991040, )

print (type, id(type), type(type))

(, 1891232, )

其中第一个print很好理解:a是一个A的实例,有自己的id(其实就是内存地址)、a的class是A。

第二个print就有点烧脑子了:A是一个class,也有自己的id(因为A也是一个对象,虽然print出来的时候没有明确说),A的class是type。

而第三个就晕乎了:type是一个type,也有自己的id(因为type也是一个对象),type的class是type,也就是它自己。

再回想上面提到的两点:A是一个对象,它的class是metaclass。也就是说 type 是一个metaclass,而A类是type类的一个对象。

唉,本来想好好解释的,没想到还是说成绕口令了。算了,反正我懂了,继续。

type

没有仔细了解type是什么的同学可能会以为type是一个函数:type(X)用于返回X的类对象。

然而并不完全是这样的:在python里,X(args)可能是调用一个函数,也可能是在实例化一个X的对象——而很不幸地,type(X)实际上是介于二者之间的一个调用:虽然type是一个class,但是它的__call__方法是存在的,于是python把它当成一个函数来调用,实际调用到了源码中的type_call;type_call调用了type.__new__试图初始化一个type类的实例,然而type.__new__(位于源码中的type_new函数)发现卧槽居然只有一个参数,于是就返回了这个参数的type(源码是这么写的:"return (PyObject *) Py_TYPE(x);",并没有生成新的对象)。也就是说,本来是个函数调用,里面却是要初始化一个对象,然而最后返回的却不是初始化的对象!尼玛那个特殊情况为毛不放到函数调用里面啊,开发者脑抽了吗!

感到脑抽的同学可以暂时忽略上面那段话,跟本文没太大关系。继续。

实际上type是在builtin模块中定义,指向源码中PyType_Type对象的一个引用:

//位于Python/bltinmodule.c
PyObject * _PyBuiltin_Init(void)
{
    ...
    SETBUILTIN("type",                  &PyType_Type);
    ...
}

这个PyType_Type又是个什么鬼?好吧,继续贴源码

//位于Objects/typeobject.c
PyTypeObject PyType_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "type",                                     /* tp_name */
    ...
    type_init,                                  /* tp_init */
    0,                                          /* tp_alloc */
    type_new,                                   /* tp_new */
    ...
};

注意3点:

PyType_Type,也就是python里的type,是在源码中生成的一个对象;这个对象的类型是PyTypeObject,所以它恰好又是一个类,至于你信不信,反正我信了。后面我把它叫做类对象,注意:不是类的对象,而是类本身是一个对象。

PyVarObject_HEAD_INIT递归引用了自己(PyType_Type)作为它的type(在源码中,指定某个对象的type为X,就是指定了它在python环境中的class为X),所以前面第三个print中可以看到,type(type) == type(哈哈哈,写绕口令真好玩)

在PyType_Type的定义指定了 tp_init = type_init 和 tp_new = type_new 这两个属性值。这是两个函数,也位于源码中的Object/typeobject.c。

关于第3点,在Python document中关于__new__方法的说明里有详细的介绍,这里简单总结一下:在new一个对象的时候,只会调用这个class的__new__方法,它需要生成一个object、调用这个对象的__init__方法对它进行初始化,然后返回这个对象。

好吧,我发现不得不把简单总结展开,否则确实说不清楚。

实例化

这是一个很有意思的设计:把实例化的流程暴露给码农,意味着码农可以在对象的生成前、生成后返回前两个环节对这个对象进行修改(【甚至】在__new__方法中生成并返回的对象并没有强制要求一定是该class的实例!不过在document里建议,如果要覆盖__new__方法,那么【应当】返回这个class的父类的__new__方法返回的对象)。这里还有一个非常tricky的地方:虽然没有明确指定,但是__new__方法被硬编码为一个staticmethod(有兴趣的话可以去翻type_new函数),它的第一个参数是需要被实例化的class,其余参数则是需要传给__init__的参数。

说起来非常枯燥,还是举一个例子吧,就用document里给出的Singleton:

class Singleton(object):
    def __new__(cls, *args, **kwargs):
        it = cls.__dict__.get("__it__")
        if it is not None:
            return it
        cls.__it__ = it = object.__new__(cls) #注意
        it.__init__(*args, **kwargs)
        return it

    def __init__(self, *args, **kwargs):
        pass

class DbConnection(Singleton):
    def __init__(self, db_config):
        self._connection = AnyHowToConnectBy(db_config)
        
conn = new DbConnection(db_config)

代码并不复杂,但是可能有点玄乎,需要理解一下那个cls参数,前面说了,它是需要被实例化的class,也就是说,最后一行实际执行的是:

DbConnection.__new__(DbConnection, db_config)

而DbConnection的__new__方法直接继承于Singleton, 所以实际调用的是

Singleton.__new__(DbConnection, db_config)

主要注意的地方,在上面这段代码的第六行,Singleton是继承于object(这里特指python中的那个object对象),因此调用了object.__new__(DbConnection)来生成一个对象,生成过程位于C源码中的object_new函数(Objects/typeobject.c),它会将新生成对象的type指定为DbConnection,然后直接返回。

Singleton.__new__在拿到了生成的DbConnection实例以后,将它保存在了DbConnection类的__it__属性中,然后对该实例进行初始化,最后返回。

可以看到,任何继承于Singleton类的子类,只要不覆盖其__new__方法,每个类永远只会被实例化一次。

好了,第2点暂告一段落,接下来回归正题,尼玛我都快忘了要讲的是metaclass啊。

metaclass

还记的上面可以暂时忽略的那段话吗?type(X)是试图实例化type对象,但是因为只有一个参数,所以源码中只是返回了X的类。而type的标准初始化参数应当有三个:class_name, bases, attributes。最前面那个"class A(object): pass",python解释器实际的流程是:

解析这段代码,得知它需要创建一个【类对象】,这个类的名字叫做"A", 它的父类列表(用tuple表示)是 (object,),它的属性用一个dict来表示就是 {} 。

查找用于生成这个类的metaclass。(终于讲到重点了有木有!)
查找过程比较蛋疼,位于Python/ceval.c : build_class函数,按顺序优先采用以下几个:

2.1 定义中使用 __metaclass__ 属性指定的(本例:没有)
2.2 如果有父类,使用第一个父类的 __class__ 属性,也就是父类的metaclass(本例:object的class,也就是type)
2.2.1 如果第一个父类没有 __class__ 属性,那就用父类的type(这是针对父类没有父类的情况)
2.3 使用当前Globals()中的 __metaclass__ 指定的(本例:没有,不过2.2里已经找到了)
2.4 使用PyClass_Type

注:2.2.1和2.4中提到了没有父类,或者父类没有父类的情形,这就是python中的old-style class,在python2.2之前所有的对象都是这样的,而2.2之后可以继承于object类,就变成了new-style class。这种设计保持了向后兼容。

使用metaclass来创建这个A类。由于A类的class就是metaclass,所以这个过程其实就是实例化metaclass的过程。本例中找到的metaclass是type,所以最终python执行的相当于这一句:

type("A", (object,), {})

再回想一下前面提到的实例化过程,实际上这一句分成两步: 1. 调用type.__new__(type, "A", (object,), {})生成type的一个实例(也就是A类对象);2. 调用type.__init__(A, "A", (object,), {}) 对A类对象进行初始化。注意:这里调用的是type.__init__,而不是A.__init__:因为A是type的一个实例。

流程终于解释完啦,不过我觉得还是举个栗子会比较好。就用我看到的那个有点二二的栗子吧:定义一个class,把它的所有属性都改成全大写的。我感觉这个栗子唯一的作用就是用来当栗子了。还好还有这个作用,否则连出生的机会都没有。

栗子

直接上代码好了:

def upper_meta(name, bases, attrs):
    new_attrs = {}
    for name, value in attrs.items():
        if not name.startswith("__"):
            new_attrs[name.upper()] = value
        else:
            new_attrs[name] = value
    return type(name, bases, new_attrs)

class Foo(object):
    __metaclass__ = upper_meta
    hello = "world"

print Foo.__dict__

请不要说“说好的metaclass呢!怎么变成了一个函数!我摔!”,回顾一下最最前面提到的一点:everything is an object in python。upper_meta作为一个函数,它也是一个对象啊。而metaclass也不过就是个对象,并没有本质上的差别——只要它被call的时候能接受name, bases, attrs这三个参数并返回一个类对象就行了。duck-typing的语言用起来就是有这样的一种不可言状的酸爽感。

理解了这一点,这段代码就能理解了,upper_meta返回了一个type类的实例——也就是Foo类,并且可以看到print出来的属性里头只有HELLO而没有hello。

考虑到可能有人不满意,想看使用class来作为metaclass的情形,我就勉为其难换个姿势再举一下这个栗子(真累)。

class upper_meta(type):
    def __new__(cls, name, bases, attrs):
        attrs = dict([(n if n.startswith("__") else n.upper(), v) for n, v in attrs.items()])
        return type(name, bases, attrs)

写的太长了,换了一个短一点的oneliner,但是效果不变(其实我就是想炫一下,不服来咬我呀)。

这段代码虽然形式上跟前面的upper_meta函数不一样,但是本质是一样的:调用了upper_meta("Foo", (object,), {"hello": "world"}),生成了一个新的名为Foo的类对象。

理论上,故事讲到这里应该结束了,然而我想说,压轴戏还没上呢。

压轴戏

我要把这栗子举得更高更远,也更符合实际开发的需求:继承。

class Bar(Foo):
    hi = "there"

print Bar.__dict__

这段代码太简单了,但是埋在下面的逻辑却太复杂了。

它的输出并不是{"HI": "there"}, 而是{"hi": "there"}。你print Bar.HELLO, Bar.__metaclass__都能得到预期的输出,但是偏偏没有HI,只有hi。

为什么?这真是个烧脑细胞的事情。我已经把所有的逻辑都展现出来了,甚至还做了特别的标记。然而即便如此,想要把这个逻辑理顺,也是一件非常有挑战性的事情,幸好我已经想明白了:苦海无涯,回头是岸。啊呸,应该是——学海无涯苦作舟,不想明白不回头。

我想说“甚至还做了特别标记”这句话的意思是,我还给【甚至】这两个字做了特别标记:在__new__方法中生成并返回的对象并没有强制要求一定是该class的实例!

问题的关键就在这里:前面两个栗子中给出的upper_meta,返回的并不是upper_meta的实例,而是type的实例,而是type的实例,而是type的实例。重说三。

什么意思?再看看代码,最后return的是type(name, bases, attrs),也就是说,Foo类对象并不是upper_meta的实例,而是type的实例(也就是说:虽然指定并被使用的metaclass是upper_meta,但是最终创建出来的Foo类的metaclass是type)。不信你print type(Foo)试试,结果就是type,而不是upper_meta。

为什么这会导致继承于Foo类的Bar类不能由upper_meta来搭建?Bar.__metaclass__不还是upper_meta吗?

这个问题就没有那么困难了,有兴趣的同学可以自己试着分析一下,没兴趣的大概也不会有耐心看到这里吧。

Bar.__metaclass__并不是Bar的原生属性,而是继承于Foo的——所以在print Bar.__dict__的时候看不到__metaclass__。也就是说,在试图创建Bar时,attrs里并没有__metaclass__属性,所以并不会直接采用upper_meta。再回顾一下选择metaclass的顺序就可以发现,实际上在2.2里会选择Foo的metaclass——Foo的metaclass是type,而不是指定的upper_meta。

解决方法很简单:关键就是前面被特别标记了的【应当】返回这个class的父类的__new__方法返回的对象。具体到代码应当是这样:

class upper_meta(type):
    def __new__(cls, name, bases, attrs):
        attrs = dict([(n if n.startswith("__") else n.upper(), v) for n, v in attrs.items()])
        return super(upper_meta, cls).__new__(cls, name, bases, attrs)

    def __init__(cls, name, bases, attrs):
        print >>sys.stderr, "in upper_meta.__init__" #FOR TEST ONLY

新增的__init__方法并不是必须的,有兴趣的同学可以跟上面的栗子对比一下,由于前面返回的是type类的实例,调用到的是type.__init__;而这样正确的写法就会调用到upper_meta.__init__。(p.s. super也是烧脑细胞的东西,但用于解决钻石继承的问很有意思,有兴趣的同学可以看看Cooperative methods and "super")

果然很烧脑细胞吧。

关于metaclass的选择,还有另外一个坑:在metaclass 2.3提到了,找不到metaclass的情况下,会使用Globals()中定义的__metaclass__属性指定的元类来创建类,那么为什么下面的代码却没有生效呢?

def __metaclass__(name, bases, attrs):
    attrs = dict([(n if n.startswith("__") else n.upper(), v) for n, v in attrs.items()])
    return type(name, bases, attrs)

class Foo(object):
    hello = "world"

print Foo.__dict__
class initializer

回到我最初的需求:我需要创建带class initializer的类。为什么会有这样的需求?最常见的metaclass的应用场景是对数据库的封装。举例来说,我希望创建一个Table类,所有表都是继承于这个类,同时我还想给每一个表都设置一个缓存dict(使用主键作为key缓存查询结果)。一个很自然的想法是这样的:

class Table(object):
    _pk_cache = {}
    
    @classmethod
    def cache(cls, obj):
        cls._pk_cache[obj.pkey()] = obj;
        
    @classmethod
    def findByPk(cls, pkey):
        return cls._pk_cache[pkey]

    def __init__(self, pkey, args):
        self._pkey = pkey
        self._args = args
        type(self).cache(self)
        
    def pkey(self):
        return self._pkey

    def __repr__(self):
        return type(self).__name__ + ":" + repr(self._args)
        
class Student(Table):
    pass
    
class Grade(Table):
    pass
    
s1 = Student(1, "s1")
g1 = Grade(1, "g1")

print Student.findByPk(1)

可惜这是错的。从输出结果就能看出来,返回的是一个Grade对象,而不是预期的Student对象。原因很简单:子类们并不直接拥有_pk_cache ,它们访问的是Table的_pk_cache ,而该dict只被初始化了一次。

当然,我可以在每一个继承于Table的class里新增一句 _pk_cache = {},但是这样的实现太丑了,而且一不注意就会漏掉导致出错。

所以我需要一个class initializer,在class被创建的时候,给它新增一个_pk_cache 。

在搞清楚了metaclass之后,解决方法特别简单:

class TableInitializer(type):
    def __new__(cls, name, bases, attrs):
        attrs["_pk_cache"] = {}
        return super(TableInitializer, cls).__new__(cls, name, bases, attrs)
        
class Table(object):
    __metaclass__ = TableInitializer

    ... #以下不变

完。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/37669.html

相关文章

  • [译]理解pythonmetaclass

    摘要:但我并不是一个翻译者并不会严格遵守每行每句的翻译有时候我会将表述换个顺序省略一些我认为无关紧要的话,以便读者更好理解。类也是对象在理解之前,我们先要掌握中的类是什么。这个对象类自身拥有产生对象实例的能力。不过这样做是为了保持向后兼容性。 前言 这篇博客是我在stackoverflow上看了一个提问回复后写的,例子基本用的都是e-satis本人的例子,语言组织也基本按照翻译来。 但我并不...

    liuchengxu 评论0 收藏0
  • How does it work - with_metaclass

    摘要:先简单介绍下中的元类。元类就是创建类的类,对于元类来说,类是它的实例,将返回。中的所有类,都是的实例,换句话说,是元类的基类。 我在看源代码的时候,经常蹦出这一句:How does it work!竟然有这种操作?本系列文章,试图剖析代码中发生的魔法。顺便作为自己的阅读笔记,以作提高。 先简单介绍下Python中的元类(metaclass)。元类就是创建类的类,对于元类来说,类是它的实...

    testbird 评论0 收藏0
  • Python -- 元类metaclass详解

    摘要:原链接中的元类是什么类也是对象在理解元类之前,需要掌握中类概念。事实上,是中用于创建所有类的元类。类本身是元类的对象在中,除了,一切皆对象,一切都是类或者元类的对象。事实上是自己的元类, 学习契机 项目中使用Elasticsearch(ES)存储海量业务数据,基于ES向外提供的API进一层封装,按需处理原始数据提供更精确、更多样化的结果。在研究这一层的代码时接触到@six.add_me...

    tracy 评论0 收藏0
  • python 类和元类(metaclass)的理解和简单运用

    摘要:什么是元类刚才说了,元类就是创建类的类。类上面的属性,相信愿意了解元类细节的盆友,都肯定见过这个东西,而且为之好奇。使用了这个魔法方法就意味着就会用指定的元类来创建类了。深刻理解中的元类 (一) python中的类 今天看到一篇好文,然后结合自己的情况总结一波。这里讨论的python类,都基于python2.7x以及继承于object的新式类进行讨论。 首先在python中,所有东西都...

    zhangqh 评论0 收藏0
  • Python: 陌生的 metaclass

    摘要:但一般情况下,我们使用类作为元类。那么,元类到底有什么用呢要你何用元类的主要目的是为了控制类的创建行为。当然,有很多种做法,这里展示用元类的做法。当你创建类时,解释器会调用元类来生成它,定义一个继承自的普通类意味着调用来创建它。 元类 Python 中的元类(metaclass)是一个深度魔法,平时我们可能比较少接触到元类,本文将通过一些简单的例子来理解这个魔法。 类也是对象 在 Py...

    miya 评论0 收藏0

发表评论

0条评论

mushang

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<