the5fire的技术博客

关注python、vim、linux、web开发和互联网--life is short, we need python.


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

作者:the5fire | 标签:         | 发布:2017-07-28 9:43 p.m. | 阅读量: 2776, 1109

之前看Django源码时没太注意metaclass是怎么做的2跟3的兼容,直到看见Django2.0dev版中只是用了Python3.x中metaclass的使用方式。

Django源码

某读者曰:喂喂,博主你上次写带那篇《如何阅读Django源代码-上篇(the5fire版) 》是不是太监了。。。

the5fire曰:不会的,下篇写到一半觉得没啥用,所以没继续写,啥时候有心情了会发出出来,让大家批评的。你瞧,这篇文章不也是个例证吗。阅读源码的几个入手点之一。

Django2.0开始不再兼容Python2.x了,因此Django2.0dev中的关于metaclass使用的代码是这样的:

class Model(metaclass=ModelBase):
    pass  # 省略其他代码-by the5fire

点击查看源码

这是Python3.x中关于metaclass的使用,在Python2.x中,我们知道metaclass的使用是需要通过__metaclass__的定义来实现的,示例代码如下:

class Model(object):
    __metaclass__ = ModelBase

在Python2.x和Python3.x之间,关于metaclass的使用,已经是完全不兼容了,从语法层面。那么问题来了,Django2.0之前的版本是如何做到兼容的???

我们来看下Django1.11中这部分代码是怎么写的,github - django源码:

class Model(six.with_metaclass(ModelBase)):
    pass  # 省略若干代码 - by the5fire

这个six.with_metaclass又是什么鬼呢?

def with_metaclass(meta, *bases):
    """Create a base class with a metaclass."""
    # This requires a bit of explanation: the basic idea is to make a dummy
    # metaclass for one level of class instantiation that replaces itself with
    # the actual metaclass.
    class metaclass(meta):
        def __new__(cls, name, this_bases, d):
            return meta(name, bases, d)
    return type.__new__(metaclass, 'temporary_class', (), {})

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

先来理解__new__这个魔术方法

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

class SingleTon(object):
    _instance = None

    def __new__(cls, *args):
        print('__new__')
        if not cls._instance:
            cls._instance = super(SingleTon, cls).__new__(cls, *args)
        return cls._instance

    def __init__(self, *args):
        self.name = 'the5fire'

single = SingleTon()  # 输出--> __new__
print(single, single.name)  # --> (<__main__.SingleTon object at 0x10c975950>, 'the5fire')
dingle = SingleTon()  # --> __new__
print(dingle, dingle.name)  # --> (<__main__.SingleTon object at 0x10c975950>, 'the5fire')

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

class Foo(object):
    def __new__(cls, *args):
        return cls  # 我们不返回实例了,直接返回类

    def __init__(self, *args):
        self.name = 'the5fire'

foo = Foo()
print(foo)  # --> <class '__main__.Foo'>

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

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

理解type

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

In [3]: type?
Docstring:
type(object) -> the object's type
type(name, bases, dict) -> a new type
Type:      type

In [4]: type.__new__?
Docstring: T.__new__(S, ...) -> a new object with type S, a subtype of T
Type:      builtin_function_or_method

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

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

# 1. 通过type创建类
def class_creator(name):
    return type(name, (), {})

Person = class_creator('Person')
print(Person, type(Person))  # --> (<class '__main__.Person'>, <type 'type'>)


# 2. 通过type.__new__创建类
def class_creator(name):
    return type.__new__(type, name, (), {})

Person = class_creator('Person')
print(Person, type(Person))  # --> (<class '__main__.Person'>, <type 'type'>)

# 3. 通过type.__new__创建一个自定义的类
class CustomClass(type):
    def __new__(cls, name, bases, attrs):
        print('in CustomClass')
        return type.__new__(cls, name, bases, attrs)


def class_creator(name):
    return type.__new__(CustomClass, name, (), {})


Person = class_creator('Person')
print(Person, type(Person))  # --> (<class '__main__.Person'>, <class '__main__.CustomClass'>)

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

其中[3]里面定义了CustomClass.__new__,你运行过上面代码后发现并没有print出来"in CustomClass"的结果。这意味着通过type.__new__创建的CustomClass的实例,并没有执行__new__这个方法。

我们继承下创建出来的这个Person试试:

class GoodPerson(Person):
    pass

# 执行下这个代码输出"in CustomClass"

为什么定义的时候就会输出"in CustomClass",也就是执行CustomClass.__new__呢,而不是像上面那样,在实例化的时候才执行?这就涉及到类创建的过程了,也就是metaclass的部分。我们稍后介绍,先来看关于type、类、实例的关系,可以从这个简略图理解下:

instance <-- class <-- type

type创建class,class创建instance或者说,class是type的实例,instance是class的实例。

对应到上面第[3]部分的代码就是,我们通过type.__new__创建了CustomClass的实例(是一个类)Person,所以可以通过GoodPerson继承Person。

理解metaclass

理解了type之后,我们再来看metaclass的使用。我们以Python2.x的方式来解释。先看个示例代码:

class metacls(type):
    def __new__(cls, name, bases, attrs):
        print(cls, name, bases, attrs)
        attrs['name'] = 'the5fire'
        return type.__new__(cls, name, bases, attrs)

class Foo(object):
    __metaclass__ = metacls

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

(<class '__main__.metacls'>, 'Foo', (<type 'object'>,), {'__module__': '__main__', '__metaclass__': <class '__main__.metacls'>})

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

__metaclass__
    This variable can be any callable accepting arguments for name, bases, and dict. Upon class creation, the callable is used instead of the built-in type().

翻译下就是,如果定义了一个可调用的``__metaclass__``变量(能接受 name, bases, dict参数)时,那这个可调用对象会替代内置的type()。也就是默认是使用type来创建类的。

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

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

# 3. 通过type.__new__创建一个自定义的类
class CustomClass(type):
    def __new__(cls, name, bases, attrs):
        print('in CustomClass')
        return type.__new__(cls, name, bases, attrs)


def class_creator(name):
    return type.__new__(CustomClass, name, (), {})


Person = class_creator('Person')
print(Person, type(Person))  # --> (<class '__main__.Person'>, <class '__main__.CustomClass'>)

class GoodPerson(Person):
    pass

# 执行下这个代码输出"in CustomClass"

我们解答上面的问题:

为什么定义的时候就会输出"in CustomClass",也就是执行``CustomClass.__new__``呢,而不是像上面那样,在实例化的时候执行?

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

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

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

对应的官网文档如下:

# the5fire: ref: https://docs.python.org/2/reference/datamodel.html#customizing-class-creation

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.

# 对应的Python源码见: https://github.com/python/cpython/blob/2.7/Python/ceval.c#L4964

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中,通过:

class Foo(object):
    __metaclass__ = MetaClass

或者Python3.x中通过:

class Foo(metaclass=MetaClass):
    pass

的定义之外,还有另外的方法来指定metaclass,也就是上一节我们讲到的。

再来回顾下Django这部分代码,把代码合在一起:

def with_metaclass(meta, *bases):
    class metaclass(meta):
        def __new__(cls, name, this_bases, d):
            return meta(name, bases, d)  # 注意不是this_bases - by the5fire.com
    return type.__new__(metaclass, 'temporary_class', (), {})

class ModelBase(type):
    def __new__(cls, name, bases, attrs):
        return type.__new__(cls, name, bases, attrs)

class Model(six.with_metaclass(ModelBase)):
    pass  # 省略若干代码 - by the5fire

这代码转换下就是:

class ModelBase(type):
    def __new__(cls, name, bases, attrs):
        return type.__new__(cls, name, bases, attrs)

class metaclass(ModelBase):
    def __new__(cls, name, this_bases, d):
        return ModelBase(name, (), d)  # 之前通过参数传递过来的``*bases``我们先去掉 - by the5fire

MetaClassForInherit = type.__new__(metaclass, 'temporary_class', (), {})

class Model(MetaClassforInherit):
    pass

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

class ModelBase(type):
    def __new__(cls, name, bases, attrs):
        print(cls, name, bases, attrs)
        # the5fire: 会输出 (<class '__main__.ModelBase'>, 'Model', (<class '__main__.temporary_class'>,), {'__module__': '__main__'})
        # 注意bases部分,多了我们中间创建的``temporary_class``。
        return type.__new__(cls, name, bases, attrs)

MetaClassForInherit = type.__new__(ModelBase, 'temporary_class', (), {})

class Model(MetaClassforInherit):
    pass

问题就是我注释地方支持的问题, 这种方式指定了metaclass之后,就无法设置子类了。

所以我们就能明白这段代码中,*bases参数,以及the5fire注释的那个位置的bases的作用了。

def with_metaclass(meta, *bases):
    class metaclass(meta):
        def __new__(cls, name, this_bases, d):
            return meta(name, bases, d)  # 注意不是this_bases - by the5fire.com
    return type.__new__(metaclass, 'temporary_class', (), {})

参考


----EOF-----

扫码关注,或者搜索微信公众号:码农悟凡


其他分类: