之前看Django源码时没太注意metaclass是怎么做的2跟3的兼容,直到看见Django2.0dev版中只是用了Python3.x中metaclass的使用方式。
Django源码
某读者曰:喂喂,博主你上次写带那篇《如何阅读Django源代码-上篇(the5fire版) 》是不是太监了。。。
the5fire曰:不会的,下篇写到一半觉得没啥用,所以没继续写,啥时候有心情了会发出出来,让大家批评的。你瞧,这篇文章不也是个例证吗。阅读源码的几个入手点之一。
Django2.0开始不再兼容Python2.x了,因此Django2.0dev中的关于metaclass使用的代码是这样的:
点击查看源码
这是Python3.x中关于metaclass的使用,在Python2.x中,我们知道metaclass的使用是需要通过__metaclass__的定义来实现的,示例代码如下:
在Python2.x和Python3.x之间,关于metaclass的使用,已经是完全不兼容了,从语法层面。那么问题来了,Django2.0之前的版本是如何做到兼容的???
我们来看下Django1.11中这部分代码是怎么写的,github - django源码:
这个six.with_metaclass又是什么鬼呢?
嗯,看起来有点复杂,不过不要紧,接下来我们慢慢捋一捋。
先来理解__new__这个魔术方法
如果你实现过单例模式,那么你一定用过它了:
从这里我们能看到,__new__是用来创建实例(instance)的,我们在这可以控制创建出什么新的实例还是复用已有的实例。但是我们可以改下__new__的返回值。
这种情况下得到的是Foo这个类,而不是实例。因此我们可以通过在__new__这个方法里折腾点东西。
这里总结下,__new__是来控制类创建的,而__init__是来控制实例初始化的。
理解type
再来看type的使用,the5fire之前写的那篇《Django分表的两个方案》有说到怎么使用type动态创建类。可以通过这案例理解type的使用。而这里的type.new是类似的,我们在ipython中看下这俩函数的说明:
type(name, bases, dict) 返回一个新的类型,继承自传递进入的bases(这个参数必须是tuple类型),以及拥有dict参数中定义的属性。 type.__new__(S, ...) 返回一个S类型的新对象,注意,这个新对象并不是我们平时写代码中的类的实例,而是类。因为S必须是type的子类(继承自type)。
我们还是来通过代码认识下:
把上面代码放到本地,运行下试试。
其中[3]里面定义了CustomClass.__new__,你运行过上面代码后发现并没有print出来"in CustomClass"的结果。这意味着通过type.__new__创建的CustomClass的实例,并没有执行__new__这个方法。
我们继承下创建出来的这个Person试试:
为什么定义的时候就会输出"in CustomClass",也就是执行CustomClass.__new__呢,而不是像上面那样,在实例化的时候才执行?这就涉及到类创建的过程了,也就是metaclass的部分。我们稍后介绍,先来看关于type、类、实例的关系,可以从这个简略图理解下:
对应到上面第[3]部分的代码就是,我们通过type.__new__创建了CustomClass的实例(是一个类)Person,所以可以通过GoodPerson继承Person。
理解metaclass
理解了type之后,我们再来看metaclass的使用。我们以Python2.x的方式来解释。先看个示例代码:
你可以把这个代码copy到你电脑上的test_meta.py文件中,然后用Python2.7运行下。会得到如下输出:
也就是说,当类定义被执行的时候,就会开始执行metaclass中的方法了,这个方法的作用是创建类。那么问题又来了,大部分情况下我们并不会定义__metaclass__,那类是怎么创建的呢?还是甩文档出来:
这个__metaclass__你可以理解为Python暴露给我们的一个接口,用来自己实现创建类的过程。在这一过程中我们可以操作即将生成的类,比如上面的代码中,metacls.__new__里面的attrs['name'] = 'the5fire'这行代码,直接增加一个类级变量。当然也可以在这里增加方法的定义。
理解到这之后,再回过头来看我们上面的逻辑,我把完整代码copy到这:
我们解答上面的问题:
这是因为类的定义(或者说创建)的过程,是通过__metaclass__来完成的,虽然我们没有显示的定义__metaclass__,但是基于下面的规则,会把我们定义的class CustomClass作为metaclass,来创建类。
metaclass查找规则:如果当前类没有__metaclass__,但是有至少一个基类,那么会去使用第一个基类的__class__作为__metaclass__,如果没有__class__则会使用type来创建类。
上面的例子中,也就是GoodPerson没有查找到__metaclass__的定义,那么就会看是否存在Person.__class__,如果存在,就把它作为__metaclass__。
对应的官网文档如下:
Python3的说明没找到,但是可以看源码:https://github.com/python/cpython/blob/master/Python/bltinmodule.c#L108 。其中的base0->ob_type可以理解为base.__class__也就是父类的类型。
再来看Django的six.with_meta代码
有了上面的认识,我们再来看Django中关于metaclass在Python2和Python3中兼容的处理就很好理解了。也就是除了在Python2中,通过:
或者Python3.x中通过:
的定义之外,还有另外的方法来指定metaclass,也就是上一节我们讲到的。
再来回顾下Django这部分代码,把代码合在一起:
这代码转换下就是:
看起来似乎没有问题,But,为啥需要两个metaclass: metaclass和ModelBase。是不是可以改成这样:
问题就是我注释地方支持的问题, 这种方式指定了metaclass之后,就无法设置子类了。
所以我们就能明白这段代码中,*bases参数,以及the5fire注释的那个位置的bases的作用了。
参考
Customizing class creation - Python2.x Docs
Customizing class creation - Python3.x Docs
what is metaclass in python
为啥不能用object.__new__替代type.__new__
《Django分表的两个方案》
- from the5fire.com
文章转自:https://www.the5fire.com/