Python 3 中的 metaclass

在 Python 程序设计语言中,大多数类用于生成对象:当你调用这些类时,它们就会返回一个该类的实例(Instance)给调用者。比如调用一个定义了学生所需各种操作的 Student 类,我们就获得了一个新的 Student 实例,这样的操作看起来非常自然。

面向对象编程(OOP)语言的一大核心思想是继承,即如果我手头有几个像 Student 一样的类,并且希望同时为这些类添加一些公用功能的话,我们会选择创建基类,然后修改这些类的定义,使这些类作为该基类的子类。而元类(Metaclass),顾名思义,意为“创建类的类”——当我们希望更深入控制一系列类的创建过程时,我们用它。

本文是中文互联网世界中又一篇讲解 Python 中元类的博客文章。如果这是你第一篇看到的相关文章,又或者你是从别的文章跳转而来,希望本文能为你打开一个理解元类的全新视角。

基类定义了一系列类的通用功能,而元类控制一系列类的创建过程。

Metaclass 与 Baseclass 的区别

从继承开始

要理解 Metaclass 的意义,让我们先从继承(Inheritance)开始。作为几乎每一门面向对象编程语言都提供的基础概念,继承操作意味着一个对象(子类)从另一个对象(基类)上获得了一系列特性。比如:

@dataclass
class People(object):
    age: int

@dataclass
class Student(People):
    grade: int

在上例中,所有 People 对象都具有 age(年龄)属性。而其子类 Student,除了具有 age 属性之外,还具有 grade(年级)属性。这就是继承:

>>> Student(11, 6)
Student(age=11, grade=6)

从 Python 2.3 开始,Python 程序设计语言采用了一种被称为“C3 算法”的方式确定一个类的 MRO(Method Resolution Order,方法解析顺序)。类的 MRO 决定了该类的继承顺序,也决定了当我们调用一个方法时,应当以什么样的顺序从基类中查找:

>>> Student.__mro__
(<class '__main__.Student'>, <class '__main__.People'>, <class 'object'>)

类的 MRO 是类本身的一种属性,而非类的实例的属性:

>>> Student(11, 6).__mro__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__mro__'

类的创建

有时我们会想要控制类的创建过程。最简单的例子是,我们希望调用 Student 类的工厂 StudentFactory 时能够返回 Student 对象,而非 StudentFactory 对象,我们如此定义:

class StudentFactory(object):
    def __new__(cls, *args, **kwargs):
        obj = object.__new__(Student)
        obj.__init__(*args, **kwargs)
        return obj

上例中,我们重载了 StudentFactory 类的 __new__ 方法,使每当我们将学生的属性传入 StudentFactory 类时,该类即返回一个新创建的 Student 实例:

>>> StudentFactory(11,6)
Student(age=11, grade=6)

同样的,对于 People 方法我们也可以构造类似的工厂类,来实现快速构造 People 类型对象。我们把两个 Factory 类放在一起:

class PeopleFactory(object):
    def __new__(cls, *args, **kwargs):
        obj = object.__new__(People)
        obj.__init__(*args, **kwargs)
        return obj

class StudentFactory(object):
    def __new__(cls, *args, **kwargs):
        obj = object.__new__(Student)
        obj.__init__(*args, **kwargs)
        return obj

有差异的部分已经使用加粗字体体现出来了。其实我们可以看到,两个类之前的差异实在是很小,如果这样类似的工厂类不是有一个、两个,而是有十个、二十个的话,我们就要写大量的重复性的代码。有没有什么方法能够批量生成类似的工厂类呢?答案是使用元类

使用元类

使用元类的基本思路是,首先要找到需要构建的类的模式。如上例中,每个 dataclass 的工厂类只有 __new__ 函数中的类型变量不同,其余全部部分都是相同的。于是,我们要构造的元类 MetaFactory 需要完成的任务就清晰了:

  • 对于每一个指定元类为 MetaFactory 的具体工厂类,都需要定义一个自己的变量,就管它叫 _instance 吧。这个变量应该是这个具体的工厂类用来生产的对象。比如:
class StudentFactory(metaclass=MetaFactory):
    _instance = Student # StudentFactory 用来生产 Student
  • 我们希望在调用具体的工厂类(比如 StudentFactory)时,能够获得对应实例类型的对象(Student)。上文中我们已经了解到,这需要通过重载 StudentFactory 类的 __new__ 函数来完成。
  • MetaFactory 需要为每一个工厂类重载它自己的 __new__ 函数

到这里,元类 MetaFactory 的作用就很清晰了:它根据需求,负责生成具体的工厂类。“需求”就是我们在每一个具体工厂类中定义的 _instance 对象。元类是有能力生成一个具体类的类。

一件小事

等等!我们还有一个小问题没有解决:上文中,我们使用了 object.__new__(Student) 方法创建了一个 Student 对象。对于元类来说,它需要创建而非类对象。这该如何做呢?

我们已经知道有一种简单的方式来创建类:

class This_Is_A_Class(object):
    a = "xxx"
    def who_am_i(self):
        return "I am a class"

在上述我们已经非常熟悉的类定义中,其实是由以下划线标注的三部分构成的:

  • 类的名称:This_Is_A_Class,字符串
  • 类的基类列表:在这里是 object 类,根据实际情况,这里有可能会被省略,也有可能出现多个类(即多重继承),所以该列表应该是个有序不可修改序列(元组)
  • 类的属性:无论类中定义了变量属性(这里的 a="xxx")还是类方法(who_am_i),这些元素都是类属性中的一部分。因为我们直接可以通过类名.属性名的方法访问到这些成员。属性列表是个键值对,使用 Python 中的字典类型表示

其实,Python 中为我们提供了另外一种创建类的方法。上述方法等价于:

This_Is_A_Class = type(
    "This_Is_A_Class",
    (object, ),
    {
        "a": "xxx",
        "who_am_i": lambda self: print("I am a class"),
    },
)

内置函数 type(name, bases, namespaces) 接收三个参数:name 是字符串,代表要定义的类名;bases 是元组,代表基类列表;namespaces 是类的属性(名称空间);该函数将类的实例本身返回并赋值给 This_Is_A_Class

无论使用上述哪种方法定义类,下述代码的执行效果是相同的:

>>> This_Is_A_Class
<class '__main__.This_Is_A_Class'>

均能返回该类本身(而非类的实例)。

工厂元类

让我们回到刚才的 MetaFactory 类的讨论。Metaclass 必须有能力返回一个具体的类,这是通过惰性求值与元类的 __new__ 方法实现的:当一个指定了元类的具体类要被使用时,如果该具体类本身还没有被计算,则以下参数会被传递给元类的 __new__ 方法:

__new__(name, bases, namespaces)

是不是很熟悉,这些参数就是调用 type() 函数创建新类所需的参数。不过工厂类必须在创建新类之前对 namespaces 进行更改:因为它需要为具体类重载对应的 __new__ 方法。完整的 MetaFactory 的定义看起来是这样的:

class MetaFactory(object):
    def factory_new(cls, *args, **kwargs):
        obj = object.__new__(cls._instance)
        obj.__init__(*args, **kwargs)
        return obj
    
    def __new__(cls, name, bases, namespaces):
        assert namespaces["_instance"]
        namespaces["__new__"] = cls.factory_new
        return type(name, bases, namespaces)

其中 __new__ 是元工厂类本身的构造函数,而 factory_new 是元工厂类为具体工厂类提供的 __new__ 函数,就像我们在文章上半部分实现的 StudentFactory 中的 __new__ 函数一样,不过类型的部分换成了 cls._instance,以适应不同的具体类型。

特别注意,如果元类继承自 type 类,则 __new__ 方法中的最后一行需要写成 return type.__new__(cls, name, bases, namespaces),否则会有一些奇怪的问题,别问我是怎么知道的。

完整的程序代码如下:

class MetaFactory(object):
    def factory_new(cls, *args, **kwargs):
        obj = object.__new__(cls._instance)
        obj.__init__(*args, **kwargs)
        return obj
	
    def __new__(cls, name, bases, namespaces):
        assert namespaces["_instance"]
        namespaces["__new__"] = cls.factory_new
        return type(name, bases, namespaces)

@dataclass
class People(object):
    age: int

@dataclass
class Student(People):
    grade: int

class PeopleFactory(metaclass=MetaFactory):
    _instance = People

class StudentFactory(metaclass=MetaFactory):
    _instance = Student

我们来调用 StudentFactory 验证一下:

>>> StudentFactory(11, 6)
Student(age=11, grade=6)

太酷了!

总结

元类是用来生成具体类的类。相比继承为类添加基础功能,元类能控制类的创建过程。根据 Python 文档,一些元类的可能使用场景包括:枚举类型、日志类、类型检查器、自动委派、代理模式、框架构建以及自动资源锁/同步逻辑等。

参考文献

坦率地说,撰写本文时,笔者参考了许多相关主题的中文和英文资料,但都不具有太大的参考价值。然而,有一些描述相关基础主题的文章很有用,列举在此处: