之前看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: metaclass
和ModelBase
。是不是可以改成这样:
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', (), {})
参考
- Customizing class creation - Python2.x Docs
- Customizing class creation - Python3.x Docs
- what is metaclass in python
- 为啥不能用
object.__new__
替代type.__new__
- 《Django分表的两个方案》
微信公众号:Python程序员杂谈