Django源码中的metaclass使用是如何兼容Python2和Python3的

之前看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又是什么鬼呢?

Django源码中的metaclass使用是如何兼容Python2和Python3的_第1张图片

嗯,看起来有点复杂,不过不要紧,接下来我们慢慢捋一捋。

先来理解__new__这个魔术方法

如果你实现过单例模式,那么你一定用过它了:

Django源码中的metaclass使用是如何兼容Python2和Python3的_第2张图片

从这里我们能看到,__new__是用来创建实例(instance)的,我们在这可以控制创建出什么新的实例还是复用已有的实例。但是我们可以改下__new__的返回值。

Django源码中的metaclass使用是如何兼容Python2和Python3的_第3张图片

这种情况下得到的是Foo这个类,而不是实例。因此我们可以通过在__new__这个方法里折腾点东西。

这里总结下,__new__是来控制类创建的,而__init__是来控制实例初始化的。

理解type

再来看type的使用,the5fire之前写的那篇《Django分表的两个方案》有说到怎么使用type动态创建类。可以通过这案例理解type的使用。而这里的type.new是类似的,我们在ipython中看下这俩函数的说明:

Django源码中的metaclass使用是如何兼容Python2和Python3的_第4张图片

type(name, bases, dict) 返回一个新的类型,继承自传递进入的bases(这个参数必须是tuple类型),以及拥有dict参数中定义的属性。 type.__new__(S, ...) 返回一个S类型的新对象,注意,这个新对象并不是我们平时写代码中的类的实例,而是类。因为S必须是type的子类(继承自type)。

我们还是来通过代码认识下:

Django源码中的metaclass使用是如何兼容Python2和Python3的_第5张图片

把上面代码放到本地,运行下试试。

其中[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的方式来解释。先看个示例代码:

Django源码中的metaclass使用是如何兼容Python2和Python3的_第6张图片

你可以把这个代码copy到你电脑上的test_meta.py文件中,然后用Python2.7运行下。会得到如下输出:

也就是说,当类定义被执行的时候,就会开始执行metaclass中的方法了,这个方法的作用是创建类。那么问题又来了,大部分情况下我们并不会定义__metaclass__,那类是怎么创建的呢?还是甩文档出来:

Django源码中的metaclass使用是如何兼容Python2和Python3的_第7张图片

这个__metaclass__你可以理解为Python暴露给我们的一个接口,用来自己实现创建类的过程。在这一过程中我们可以操作即将生成的类,比如上面的代码中,metacls.__new__里面的attrs['name'] = 'the5fire'这行代码,直接增加一个类级变量。当然也可以在这里增加方法的定义。

理解到这之后,再回过头来看我们上面的逻辑,我把完整代码copy到这:

Django源码中的metaclass使用是如何兼容Python2和Python3的_第8张图片

我们解答上面的问题:

这是因为类的定义(或者说创建)的过程,是通过__metaclass__来完成的,虽然我们没有显示的定义__metaclass__,但是基于下面的规则,会把我们定义的class CustomClass作为metaclass,来创建类。

metaclass查找规则:如果当前类没有__metaclass__,但是有至少一个基类,那么会去使用第一个基类的__class__作为__metaclass__,如果没有__class__则会使用type来创建类。

上面的例子中,也就是GoodPerson没有查找到__metaclass__的定义,那么就会看是否存在Person.__class__,如果存在,就把它作为__metaclass__。

对应的官网文档如下:

Django源码中的metaclass使用是如何兼容Python2和Python3的_第9张图片

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这部分代码,把代码合在一起:

Django源码中的metaclass使用是如何兼容Python2和Python3的_第10张图片

这代码转换下就是:

Django源码中的metaclass使用是如何兼容Python2和Python3的_第11张图片

看起来似乎没有问题,But,为啥需要两个metaclass: metaclass和ModelBase。是不是可以改成这样:

Django源码中的metaclass使用是如何兼容Python2和Python3的_第12张图片

问题就是我注释地方支持的问题, 这种方式指定了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/

Django源码中的metaclass使用是如何兼容Python2和Python3的_第13张图片

你可能感兴趣的:(Django源码中的metaclass使用是如何兼容Python2和Python3的)