Python 元类及with_metaclass

python使用__new__和__init_来创建和初始化一个对象[4]。

在基础类object中,__new__被定义成了一个静态方法,并且需要传递一个参数cls。cls表示需要实例化的类,此参数在实例化时由Python解析器自动提供。__new__会返回一个对象,传输给__init__的self。

class Test(object):
    # 1. 创建对象
    def __new__(cls):  
        print("new  ", cls)
        return object.__new__(cls)  # 必须有return
    # 2. 将创建的对象传给self,init不需要返回值
    def __init__(self):
        print("init ", self)
        object.__init__(self)

# 实例化类
tt = Test()
# 输出(注意二者顺序):
# new   <class '__main__.Test'>
# init  <__main__.Test object at 0x7fc1aa0dbe50>

元类(metaclass)可以控制类的创建行为,元类生成的实例是类。那么怎么创建metaclass呢?——以type为基类。定义一个类,指定它的元类,就可以通过元类对类进行修改。

在python3里面[3],首先定义元类TestMeta3和要继承的父类Pa3:

class TestMeta3(type):
    def __new__(cls, name,bases,attrs):
        print(cls)
        print(name) 
        print(bases)
        print(attrs)
        return type.__new__(cls,name,bases,attrs)
class Pa3:
    pass

接着定义带有元类和父类的类:

class Eg3(Pa3, metaclass=TestMeta3):
    @classmethod
    def get(self):
        kkk=[]
        kkk.append(self.__skiless__)
        return kkk
    def acc2(self):
        return 'a2'
# 输出
# <class '__main__.TestMeta3'>
# Eg3
# (<class '__main__.Pa3'>,)
# {'__module__': '__main__', '__qualname__': 'Eg3', 'get': <classmethod object at 0x7fc1aa0cbed0>, 'acc2': <function Eg3.acc2 at 0x7fc1aa1063b0>}

在定义的时候,发现竟然有输出。因为定义的时候,python解释器会在当前类中查找metaclass[3],如果找到了,就使用该metaclass创建Eg3类。所以打印出来的name、bases、attrs都和Eg3有关。

由于python2和python3中元类使用方法的不同,我们需要使用一种兼容的方式[1],如下所示:

def with_metaclass(meta, *bases):
    print("with metaclass")
    return meta('temp_class', bases, {})

class TestMeta(type):
    def __new__(cls, name, bases, d):
        d['a'] = 'xyz'
        print("new  ", cls, name, bases)
        return type.__new__(cls, name, bases, d)
    def __init__(cls, name, bases, d):
        print("init ", cls, name, bases)
        type.__init__(cls, name, bases, d)

class Foo(object):pass

temp = with_metaclass(TestMeta, Foo)
# 输出:
# with metaclass
# new   <class '__main__.TestMeta'> temp_class (<class '__main__.Foo'>,)
# init  <class '__main__.temp_class'> temp_class (<class '__main__.Foo'>,)

为了创建temp,先调用with_metaclass(meta,*bases),然后调用meta('temp_class', bases,{}),因为meta此时对应TestMeta,所以调用TestMeta('temp_class', bases,{}),输出<class '__main__.TestMeta'> temp_class (<class '__main_.Foo'>,)。

这样我们就得到了一个名为temp_class的类temp,以TestMeta为元类,以Foo为父类,这个类是TestMeta创建出来的。但是这样不方便对它定义函数,所以我们新定义一个class Bar,继承temp_class。

class Bar(temp): pass
# 输出:
# new   <class '__main__.TestMeta'> Bar (<class '__main__.temp_class'>,)
# init  <class '__main__.Bar'> Bar (<class '__main__.temp_class'>,)

当用户定义 Bar(temp) 时,Python解释器首先在当前类Bar的定义中查找metaclass,如果没有找到,就继续在父类temp中查找metaclass,找到了,就使用temp的metaclass来创建Bar类,也就是说,metaclass可以隐式地继承到子类,但子类自己却感觉不到[3]。

这样我们就得到了一个以TestMeta为元类,继承Foo的名为Bar的类。

但我们看到在Bar的继承关系(使用Bar.__mro__查看)里混进了一个临时类temp_class,你忽略它吧,有时会很麻烦。作为完美主义者,我想寻找一种解决办法,不要在mro中引入多余的类。Python的six模块专门为解决Python 2to3兼容问题而生,模块里带有一个with_metaclass函数[1]。

# 这里的meta继承type类,是一个元类
def with_metaclass(meta, *bases):
    class metaclass(meta):
        # 注意这里的this_bases被省略了
        def __new__(cls, name, this_bases, d):
            print("m new  ",cls, name, this_bases)
            return meta(name, bases, d)

    # 创建一个名为temp_class的类,这个类未被初始化
    return type.__new__(metaclass, 'temp_class', (), {})  
    # 注意,type.__new__()需要与type()区分 [2]

# 一个名为temp_class的类,是元类metaclass的实例,和TestMeta、Foo无关
temp = with_metaclass(TestMeta, Foo)  
class Bar(temp): pass
# 输出
# m new   <class '__main__.with_metaclass.<locals>.metaclass'> Bar (<class '__main__.temp_class'>,)
# new   <class '__main__.TestMeta'> Bar (<class '__main__.Foo'>,)
# init  <class '__main__.Bar'> Bar (<class '__main__.Foo'>,)

这个with_metaclass返回type.__new__(metaclass,'temp_class',(),{}),也就是使用metaclass元类创建一个名为temp_class的类,返回的这个类此时还没有进行__init__。

接着,定义Bar,因为Bar继承的temp是元类metaclass创建的,所以:

  1. 首先会找到metaclass的定义,实例化metaclass,也就是调用metaclass的__new__。
  2. 当前的this_bases是'temp_class',bases才是真正的父类,所以直接跳过this_bases,以bases作为父类,即meta(name, bases, d)。
  3. meta指的是TestMeta,因此meta(name, bases, d即TestMeta(name, bases, d)。

但是为什么实例化metaclass的时候没有调用metaclass的__init_呢?因为metaclass的__new返回meta的实例,而在一个类中,返回其他类的实例会跳过__init_,所以这里没有执行__init__。

with_metaclass返回的临时类中,本身无任何属性,但包含了元类和基类的所有信息,并在下一步定义类时将所有信息解包出来[1]。


参考:

[1] How does it work - with_metaclass

[2] stackoverflow.com/quest

[3] 使用元类 - Python3中文手册

[4] 3. Data model - Python 3.9.2 documentation

发布于 2021-03-06 21:15