揭开Python元类(metaclass)神秘的面纱

Python语言的metaclass特性一直是初学者的”噩梦”,当初博主在学习元类时也是一头雾水,但是一旦真正的理解了什么是”动态语言”之后,元类就不再神秘与难以理解了。Python这门动态语言最大的特性就是不需要一个类的字节码就能够在运行时创建出一个类,这是理解元类最为关键的信息。

1. 基础知识汇总

1.1 stackoverflow

首先,强烈推荐阅读stackoverflow上关于metaclass的回答,作者并没有使用什么高级词汇,就算英语稀烂也能看的懂。

https://stackoverflow.com/a/6581949/12523821

1.2 类属性和实例属性

类属性表示绑定在一个类上的属性,而实例属性则是绑定在不同实例上的属性,类属性只有一份,而实例属性则可以有多份。当实例属性和类属性重名,并通过实例获取该属性时,会返回实例属性,而不是类属性。

class Hugo(object):
    name = None
    def __init__(self, name):
        self.name = name

if __name__ == "__main__":
    Hugo.name = "smart"
    print(Hugo.name)      # "smart"

    hugo = Hugo("raven")  
    print(hugo.name)      # "raven"

    print(Hugo.name)      # "smart"

1.3 __new__方法和__init__方法

在Python中,实际创建对象的过程是由__new__方法控制的,该方法接收class对象(cls)。而__init__方法则是在__new__方法所创建的对象实例上,进行属性的赋值或者其它操作,所以接收实例对象(self)。

当想要控制创建对象的过程时,应该使用__new__方法,例如常用的单例模式,而不是使用__init__方法:

from threading import Lock

class SingletonClass(object):
    instance = None
    lock = Lock()
    
    def __new__(cls, *args, **kwargs):
        if cls.instance:
            return cls.instance
        with cls.lock:
            # double check
            if not cls.instance:
                cls.instance = object.__new__(cls)
            return cls.instance

1.4 MRO

Python是通过MRO列表来实现类的继承的,MRO列表的构造由C3线性化算法实现。实际上,类的继承层级关系最终会表现成包含所有基类的线性顺序表。

class Parent(object):
    def __init__(self):
        print("Parent init")

class Children(Parent):
    def __init__(self):
        super(Children, self).__init__()
        print("Children init")

class Grandchildren(Children):
    def __init__(self):
        super(Grandchildren, self).__init__()
        print("Grandchildren init")

if __name__ == "__main__":
    print(Grandchildren.__mro__)

运行结果为:

(
    <class '__main__.Grandchildren'>, 
    <class '__main__.Children'>, 
    <class '__main__.Parent'>, 
    <class 'object'>
)

其顺序与继承顺序刚好相反,也就是说,通过类的__mro__属性即可找到该类的所有父类,包括object类。

Python同时也提供了内建的反射函数,来返回某个类的MRO列表:

def getmro(cls):
    return cls.__mro__

2. metaclass

我们已经知道了metaclass是创建一个类的工具,通过metaclass能够更加灵活地动态地创建一个类,其中一个非常重要的结果就是能够获取到”子类”的全部信息,例如类属性、类方法等。

class HugoMetaclass(type):
    def __new__(mcs, name, bases, attrs):
    
        for name, value in attrs.items():
            print(f"get class field: {name}===>{value}")
            
        return super().__new__(mcs, name, bases, attrs)

class Hugo(metaclass=HugoMetaclass):
    name = "smart"
    gender = "male"

运行上述代码将会打印出Hugo类的所有属性信息:

get class field: __module__===>__main__
get class field: __qualname__===>Hugo
get class field: name===>smart
get class field: gender===>male

其中__module____qualname__为内部属性,而namegender则是用户自定义的类属性。可以看到,在HugoMetaclass。__new__方法中,完全能够获取到Hugo类的相关类属性,那么更进一步地来说,不管用户定义了什么样的类属性,都可以使用metaclass在创建该类之前获取到该类的所有属性。这就为诸如ORM、表单验证等基础服务提供了构建的基础。

2.1 metaclass的应用

type__new__方法接收4个参数,分别为类对象,类名称,父类元组以及类属性。这四个参数中最为关键的就是父类元组和类属性,通常项目中使用metaclass时也是和这两个参数频繁打交道。

2.1.1 父类元组
class HugoMetaclass(type):
    def __new__(mcs, name, bases, attrs):
        print(bases)
        return super().__new__(mcs, name, bases, attrs)

class Hugo(metaclass=HugoMetaclass):
    pass

class HugoChild(Hugo):
    pass

运行后将得到以下结果:

()
(<class '__main__.Hugo'>,)

一共需要创建两个类: HugoHugoChildHugo类直接使用HugoMetaclass创建,所以其父类元组为空。而HugoChild直接继承自Hugo,所以其父类为Hugo。所以,可以通过bases参数来判断当前创建的类是否需要进行处理。

class HugoMetaclass(type):
    def __new__(mcs, name, bases, attrs):
    
        parents = [b for b in bases if isinstance(b, HugoMetaclass)]
        if not parents:
            return super().__new__(mcs, name, bases, attrs)

        # 这里所创建的类都是Hugo的子类, 而不是Hugo类
        return super().__new__(mcs, name, bases, attrs)
2.1.2 类属性

类属性是”子类”中最为重要的数据,可以说元类的最终目的就是为了根据类属性创建出一个模板,将该模板数据保存在类中。

class HugoMetaclass(type):
    def __new__(mcs, name, bases, attrs):

        parents = [b for b in bases if isinstance(b, HugoMetaclass)]

        # 对Hugo类不做任何处理
        if not parents:
            return super().__new__(mcs, name, bases, attrs)

        klass = super().__new__(mcs, name, bases, attrs)

        # 保存attrs中所有的int类型数据
        klass.declared_fields = {}

        for name, value in attrs.items():
            if isinstance(value, int):
                klass.declared_fields[name] = value

        return klass
        
class Hugo(metaclass=HugoMetaclass):
    pass

class HugoChild(Hugo):
    name = "smart"
    age = 24

if __name__ == "__main__":
    print(HugoChild.declared_fields)

上面创建了一个int类型的”模板”,并保存在了declared_fields这个字典中。注意不要将declared_fields挂到mcs上,mcs就是HugoMetaclass,变量绑定到mcs上会丢失一些信息,导致程序出现BUG。

那么如果HugoChild又有子类呢? 上述方式是否能够将HugoChild和其子类的属性一起获取到呢?

class HugoChild(Hugo):
    name = "smart"
    age = 24

class HugoGrandChild(HugoChild):
    height = 180

if __name__ == "__main__":
    print(Hugo.declared_fields)
    print(HugoChild.declared_fields)
    print(HugoGrandChild.declared_fields)

这时候会发现,这三个类的declared_fields结果都是{'height': 180}age字段丢失了。原因也很简单,在创建HugoGrandChild类时,declared_fields被重新声明成了空字典,所以HugoChild中的类属性就会丢失。那么有没有什么办法能够得到完整版呢? 这就需要用到上面所提到的MRO列表了。

我们可以通过MRO列表,来获取到HugoGrandChild的所有父类,而后逐一的遍历找出类型为int的类属性,保存在declared_fields这个字典中。

class HugoMetaclass(type):
    def __new__(mcs, name, bases, attrs):

        parents = [b for b in bases if isinstance(b, HugoMetaclass)]

        # 对Hugo类不做任何处理
        if not parents:
            return super().__new__(mcs, name, bases, attrs)

        # 保存attrs中所有的int类型数据
        klass = super().__new__(mcs, name, bases, attrs)

        klass.declared_fields = {}

        for name, value in attrs.items():
            if isinstance(value, int):
                klass.declared_fields[name] = value

        # 遍历__mro__列表并找出类型为`int`的类属性, 保存在字典中
        for parent in klass.__mro__:
            for name, value in getattr(parent, 'declared_fields', parent.__dict__).items():
                if isinstance(value, int):
                    klass.declared_fields[name] = value

        return klass

if __name__ == "__main__":
    print(HugoChild.declared_fields)
    print(HugoGrandChild.declared_fields)

其运行结果为:

{'age': 24}
{'height': 180, 'age': 24}

如此一来,HugoGrandChild在继承了HugoChild之后,也能够获取到其中的相关字段,并且父类不会受到子类的影响。

上述代码中存在一些重复的代码片段,将其抽离出来,使代码结构更加清晰:

def is_instance_or_subclass(val, class_):
    try:
        return issubclass(val, class_)
    except TypeError:
        return isinstance(val, class_)

def _get_fields(attrs, field_class):
    fields = [
        (field_name, attrs.get(field_name))
        for field_name, field_value in list(attrs.items())
        if is_instance_or_subclass(field_value, field_class)
    ]
    return fields

def _get_fields_by_mro(klass, field_class):
    mro = klass.__mro__
    return sum(
        (
            _get_fields(
                getattr(base, 'declared_fields', base.__dict__),
                field_class,
            )
            for base in mro[:0:-1]
        ),
        [],
    )

class HugoMetaclass(type):
    def __new__(mcs, name, bases, attrs):

        parents = [b for b in bases if isinstance(b, HugoMetaclass)]

        # 对Hugo类不做任何处理
        if not parents:
            return super().__new__(mcs, name, bases, attrs)

        # 保存attrs中所有的int类型数据
        klass = super().__new__(mcs, name, bases, attrs)

        class_fields = _get_fields(attrs, int)
        inherited_fields = _get_fields_by_mro(klass, int)
        klass.declared_fields = dict(class_fields + inherited_fields)

        return klass

3. 小结

metaclass并不神秘,得益于Python是动态语言,可以在运行时动态地创建一个类的特性,我们能够在事前去创建一些有用的”模板”,在运行时将模板和数据有机的结合起来,最终呈现出宛如魔术般的效果。

SmartKeyerror

日拱一卒,功不唐捐