在 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 文档,一些元类的可能使用场景包括:枚举类型、日志类、类型检查器、自动委派、代理模式、框架构建以及自动资源锁/同步逻辑等。
参考文献
坦率地说,撰写本文时,笔者参考了许多相关主题的中文和英文资料,但都不具有太大的参考价值。然而,有一些描述相关基础主题的文章很有用,列举在此处:
- Tendai Mutunhire 的《Python Metaclasses and Metaprogramming》一文
- 来自 GeeksforGeeks 的 《__new__ in Python》一文
- Python 3 文档中关于 Metaclasses 部分的描述