探索Python中神奇的元类:解密Metaclass的奥秘

83 阅读7分钟

前言

之前在介绍单例模式的实现方法时,其中一种方法是通过metaclass实现的,这篇文章主要介绍metaclass

内置函数type()

这里我们从type入手,详细了解。相信很多同学都使用过type(),用来查看一个对象的类型。

class classA:
    name = "type test"a = classA()
b = 3.0
​
print(type(a)) # <class '__main__.classA'>
print(type(b)) # <class 'float'>

当然可能有些同学是这样看对象类型的

print(a.__class__) # <class '__main__.classA'>
print(b.__class__) # <class 'float'>

可以看到,此时type()object.__class__的功能相同,都是返回对象的类型

翻开源码看看,type居然还可以创建一个类

class type(object):
    """
    type(object_or_name, bases, dict)
    type(object) -> the object's type
    type(name, bases, dict) -> a new type
    """
    ......

从源码注解中可以看到,当传入三个参数时,用来创建一个类。我们看下面这个例子

ClassA = type('ClassA', (object,), dict(name = "type test"))
a = ClassA()
print(type(a)) # <class '__main__.ClassA'>
print(a.name) # type test

可以看到已经成功创建了ClassA类型的类

通常,我们都是用class xxx来定义一个类的;但是type()函数也允许我们动态的创建一个类。 python是一种解释型的动态语言,动态语言与静态语言最大的区别是,可以方便的在运行期间动态的创建类。

感兴趣的同学,也可以看看底层源码

cpython/Objects/typeobject.c

static PyObject *
type_call(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    /* 检查参数 */
    if (!PyType_Check(type)) {
        PyErr_Format(PyExc_TypeError,
                     "descriptor '__call__' requires a 'type' object but "
                     "received an '%.200s'",
                     type->tp_name);
        return NULL;
    }
    
    /* 创建一个新对象 */
    PyObject *obj = type->tp_new(type, args, kwds);
    if (obj == NULL)
        return NULL;
    
    /* 初始化对象 */
    int init_result = type->tp_init(obj, args, kwds);
    if (init_result < 0) {
        Py_DECREF(obj);
        return NULL;
    }
    
    return obj;
}

上述源码是 type 函数的一个简化版本,在实际的源码中还包含了更多的逻辑和处理细节。这段代码主要进行了以下操作:

  1. 检查传入的 type 参数是否是一个类型对象。
  2. 使用类型对象的 tp_new 方法创建一个新的对象。
  3. 调用对象的 tp_init 方法初始化对象。
  4. 返回创建并初始化后的对象。

metaclass

进入正题,看看到底啥是metaclass呢?metaclas直译为元类,可控制类的属性和类实例的创建过程。

在 Python 中,使用元类的常见方式是通过在类定义的时候将元类指定为所谓的 __metaclass__ 属性。例如:

class MyClass(metaclass=MyMetaClass):
    pass

在上述示例中,MyMetaClass 将作为 MyClass 类的元类,它负责控制 MyClass 类的创建和行为。

python中,一切都可以是对象:一个整数、一串字符串、一个类实例、类本身都是对象。 一个类也是一个对象,他是元类(metaclass)的一个实例。看迷糊了吧,我们看个例子,就会清晰很多

class MyClass:
    pass
​
m = MyClass()
print(type(MyClass)) # <class 'type'>
print(type(m)) # <class '__main__.MyClass'>
print(isinstance(m, MyClass)) # True
print(isinstance(MyClass, type)) # True

默认的metaclass是type类型的,所以我们看到MyClass的类型是type。(MyClass是metaclass的一个实例) type在python中是一个极为特殊的类型。为了彻底理解metaclass,我们先搞清楚type与object的关系。

type与object的关系

在python3中,object是所有类的基类,内置的类、自定义的类都直接或间接的继承自object类。如果去看源码,会发现type类也继承自object类。这对 我们的理解造成了极大的困扰,主要以下三点:

  • type是object的类型,object是type的一个实例
  • type是object的一个子类,继承object的所有属性和行为
  • type还是一个callable,即实现了__call__方法,可以当成一个函数使用。

type与object有点像"蛋生鸡"与"鸡生蛋"的关系,type是object的子类, 同时object又是type的一个实例(type是object的类型),二者是不可分离的 type的类型也是type,先记住吧。

print(type(object)) # <class 'type'>
print(type(type)) # <class 'type'>

我们可以自定义metaclass,自定义的metaclass必须继承自type。 一般来说,类class的类型为type(即一般的类的metaclass是type,是type的一个实例)。如果要改变类的metaclass,必须在定义类时 显示地指定他的metaclass

class CustomMetaClass(type):
    passclass CustomClass(metaclass=CustomMetaClass):
    pass
​
obj = CustomClass()
print(type(CustomClass)) # <class 'CustomMetaClass'>
print(type(obj)) # <class '__main__.CustomClass'>
print(isinstance(obj, CustomClass)) # True
print(isinstance(obj, object)) # True

自定义metaclass

class CustomMetaClass(type):
​
    def __init__(cls, what, bases=None, dict=None):
        print("CustomMetaClass.__init__ cls:", cls)
        super().__init__(what, bases, dict)
​
    def __call__(cls, *args, **kwargs):
        print("CustomMetaClass.__call__ args:", args, kwargs)
        self = super(CustomMetaClass, cls).__call__(*args, **kwargs)
        print("CustomMetaClass.__call__ self:", self)
        return self
​
class CustomClass(metaclass=CustomMetaClass):
​
    def __init__(self, *args, **kwargs):
        print("CustomClass.__init__ self:", self)
        super().__init__()
​
    def __new__(cls, *args, **kwargs):
        self = super().__new__(cls)
        print("CustomClass.__new__ self:", self)
        return self
​
    def __call__(self, *args, **kwargs):
        print("CustomClass.__call__ args:", args)
​
​
obj = CustomClass("Meta arg1", "Meta arg2", kwarg1=1, kwarg2=2)
​
​
"""
CustomMetaClass.__init__ cls: <class '__main__.CustomClass'>
CustomMetaClass.__call__ args: ('Meta arg1', 'Meta arg2') {'kwarg1': 1, 'kwarg2': 2}
CustomClass.__new__ self: <__main__.CustomClass object at 0x11b73b5e0>
CustomClass.__init__ self: <__main__.CustomClass object at 0x11b73b5e0>
CustomMetaClass.__call__ self: <__main__.CustomClass object at 0x11b73b5e0>
"""

实例对象的整个创建过程大致是这样的:

  1. metaclass.__init__进行一些初始化的操作,如一些全局变量的初始化
  2. metaclass.__call__创建实例,在创建的过程中会调用class的__new____init__方法
  3. class.__new__进行具体的实例化的操作,并返回实例对象obj(0x11b73b5e0)
  4. class.__init__对返回的实例对象obj(0x11b73b5e0)进行初始化,如一些状态和属性的设置
  5. 返回一个用户真正需要使用的对象obj(0x11b73b5e0)

到这里我们应该知道了,通过metaclass几乎可以自定义一个对象生命周期的各个过程。

注意:

  • object的__init__方法只有1个参数,但自定义的metaclass的__init__有4个参数 object的__init__方法只有1个参数:def __init__(self),但type重写了__init__方法,有4个参数:def __init__(cls, what, bases=None, dict=None): 因为自定义metaclass继承自type,所以重写init方法时也要4个参数
  • 对于普通的类,重写__call__方法说明对象是callable的。在metaclass中__call__方法还负责对象的创建。

实战

示例一

看下面这段代码

import yaml
​
class Monster(yaml.YAMLObject):
  yaml_tag = '!Monster'
  
  def __init__(self, name, hp, ac, attacks):
    self.name = name
    self.hp = hp
    self.ac = ac
    self.attacks = attacks
    
  def __repr__(self):
    return f"({self.__class__.__name__}name={self.name}, hp={self.hp}, ac={self.ac}, attacks={self.attacks})"
​
m = yaml.load("""
--- !Monster
name: Cave spider
hp: [2,6]    # 2d6
ac: 16
attacks: [BITE, HURT]
""", Loader=yaml.Loader)
print(m) # (Monstername=Cave spider, hp=[2, 6], ac=16, attacks=['BITE', 'HURT'])
​
Monster(name='Cave spider', hp=[2, 6], ac=16, attacks=['BITE', 'HURT'])
print(yaml.dump(Monster(
    name='Cave lizard', hp=[3,6], ac=16, attacks=['BITE','HURT'])))
'''
!Monster
ac: 16
attacks:
- BITE
- HURT
hp:
- 3
- 6
name: Cave lizard
'''

这段代码是一个使用 PyYAML 库的示例,它演示了如何使用 YAML 格式来定义(Monster)对象,并使用 PyYAML 的 load() 方法将 YAML 数据加载为 Monster 类的实例。

1、定义 Monster 类:class Monster(yaml.YAMLObject) 定义了一个名为 Monster 的类。通过继承 yaml.YAMLObject 类并设置 yaml_tag 属性为 !Monster,将 Monster 类与 YAML 中的 !Monster 标签关联起来。

2、Monster 类的构造函数:def __init__(self, name, hp, ac, attacks) 是 Monster 类的构造函数,用于初始化 Monster 对象的属性。name 表示怪物的名称,hp 表示生命值,ac 表示防御等级,attacks 表示攻击方式。

3、Monster 类的显示方法:def __repr__(self) 定义了 Monster 类的显示方法,用于返回类对象的字符串表示。

4、使用 yaml.load() 加载 YAML 数据:yaml.load("""...""", Loader=yaml.Loader) 使用 PyYAML 的 load() 方法加载 YAML 数据。"""...""" 之间的内容是 YAML 格式的数据,表示一个Monster对象的属性。Loader=yaml.Loader 指定使用 PyYAML 的 Loader 加载器来解析 YAML 数据。

5、最后,代码会将 YAML 数据加载为 Monster 类的实例,并打印该对象的字符串表示。

可以看到,只要简单的继承yaml.YAMLObject,就可以让普通的Python Object具有序列化和逆序列化能力。

YAML 的这种动态序列化 / 逆序列化功能正是用 metaclass 实现的。

源码

class YAMLObjectMetaclass(type):
    """
    The metaclass for YAMLObject.
    """
    def __init__(cls, name, bases, kwds):
        super(YAMLObjectMetaclass, cls).__init__(name, bases, kwds)
        if 'yaml_tag' in kwds and kwds['yaml_tag'] is not None:
            if isinstance(cls.yaml_loader, list):
                for loader in cls.yaml_loader:
                    loader.add_constructor(cls.yaml_tag, cls.from_yaml)
            else:
                cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml)
​
            cls.yaml_dumper.add_representer(cls, cls.to_yaml)
​
class YAMLObject(metaclass=YAMLObjectMetaclass):
      ......

如何将将 Monster 类与 YAML 中的 !Monster 标签关联起来呢?

一个简单的做法就是建立一个全局变量registry,把所有需要逆序列化的 YAMLObject,都注册进去。比如下面这样:

registry = {}
​
def add_constructor(target_class):
    registry[target_class.yaml_tag] = target_class

然后,在 Monster 类定义后面加上下面这行代码:

add_constructor(Monster)

很明显,这会现的很麻烦,而且也很容易忘记。那YAML是如何做的呢?看上面的源码,正是使用了metaclass解决该问题

解释一下上面的源码:

  1. 定义 YAMLObjectMetaclass 类:class YAMLObjectMetaclass(type) 定义了一个名为 YAMLObjectMetaclass 的元类(metaclass),它继承自内建的 type 类。

  2. 初始化方法:def __init__(cls, name, bases, kwds) 是 YAMLObjectMetaclass 类的初始化方法。在此方法中,首先调用父类的初始化方法 super(YAMLObjectMetaclass, cls).__init__(name, bases, kwds) 来确保正确地初始化当前类。然后检查是否定义了 yaml_tag 属性,并且该属性不为 None。

  3. 注册构造函数和序列化器:根据 yaml_tag 属性的值,将当前类的 from_yaml() 方法注册为解析器的构造函数,将当前类的 to_yaml() 方法注册为序列化器的表示器。

    • 如果 yaml_loader 属性是一个列表(list),则遍历列表中的每个加载器(loader),通过 add_constructor() 方法将当前类的 yaml_tag 和 from_yaml() 方法关联起来。
    • 如果 yaml_loader 属性不是列表,则假定它是一个加载器对象,直接使用 add_constructor() 方法将当前类的 yaml_tag 和 from_yaml() 方法关联起来。

    最后,使用 cls.yaml_dumper.add_representer() 方法将当前类和 to_yaml() 方法关联起来,以便在将对象序列化为 YAML 数据时调用 to_yaml() 方法。

YAML 应用 metaclass,拦截了所有 YAMLObject 子类的定义。也就是说,在你定义任何 YAMLObject 子类时,Python 会强行插入运行下面这段代码,把我们之前想要的add_constructor(Monster)给自动加上。这样开发者使用起来就简单很多。

示例二

用来实现单例模式

class Singleton(type):
​
    def __init__(cls, what, bases=None, dict=None):
        super().__init__(what, bases, dict)
        cls._instance = None 
​
    def __call__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__call__(*args, **kwargs)
        return cls._instance
​
class CustomClass(metaclass=Singleton):
   
    def __init__(self, name):
        self.__name = name
​
    def getName(self):
        return self.__name
​

最后

metaclass要慎用,它会改变类的行为,使用不当,可能造成很大的风险。一般应用层,metaclass都用不到。这里主要作为一个了解,知道python的魔法特性。

单例模式的魅力与实践