metaclass 在程序开发中的妙用

读万卷书,行万里路。

—— 佚名

Python 类型系统中,元类 ( metaclass )占据着至关重要的位置。它提供了制造新类型的能力,为程序设计带来更多可能性。不少功能强大的开发框架,内部实现离不开元类的强力加持。

然而,元类由于天生的抽象性,让很多人望而却步。本节我们以若干应用场景抛砖引玉,带你拿下元类这个新技能。

new 魔术方法

我们先考察 new 魔术方法,它负责为类创建实例对象。什么?实例对象不是由 init 魔术方法创建吗?为了拨开心中的迷雾,我们先将这两者的区别和联系搞清楚。

当我们调用类 C 创建实例对象时,Python 调用 C.new 魔法函数完成对象创建:

1
instance = C.__new__()

如果 C 类并没有定义 new 方法,Python 将使用父类的 new 方法。如果父类也没有实现该方法,Python 将逐层上溯,直到终极父类 object ,而 object 提供了通用的 new 版本进行兜底。

将实例对象 instance 返回给调用者之前,C.new 调用 instance.init 魔术方法对实例对象进行初始化:

1
instance.__init__()
  • new ,负责为创建实例对象(分配内存);
  • init ,负责对实例对象进行初始化;

由于 init 魔术方法执行时,实例对象 self 已经创建好了,因此无法对创建过程施加影响。另一方面,new 魔术方法刚执行时,实例对象还未诞生,因此它不可能以实例方法的形式存在,只能以类方法的形式存在。

那么,有什么场景需要应用 new 魔术方法呢?—— 最典型的例子是:单例模式 的实现。

举个例子,假设我们有一个计数器类 Counter 用于统计应用访问量:

1
2
3
4
5
6
7
8
9
class Counter:

    def __init__(self):
        self.value = 0
        self.lock = Lock()

    def inc(self):
        with self.lock:
            self.value += 1

在程序很多地方都会调用计数器的 inc 方法,对计数器进行自增:

1
2
counter = Counter()
counter.inc()

由于计数器全局只需一个,我们希望把它做成单例。换句话讲,不管我们调用 Counter 多少次,它都返回全局唯一的一个 Counter 实例:

1
>>> Counter() is Counter()

因此,当 Counter 创建第一个实例后,我们需要将它记录起来;后续再调用 Counter ,直接将其返回。结合 newinit 这两种魔术方法的运行机制,我们知道应该在 new 方法中做文章:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Counter:

    instance = None

    def __new__(cls, *args, **kwargs):
        if cls.instance is None:
            cls.instance = super().__new__(cls, *args, **kwargs)

        return cls.instance

    def __init__(self):
        self.value = 0
        self.lock = Lock()

    def inc(self):
        with self.lock:
            self.value += 1

我们引入类属性 instance ,用于记录唯一的实例对象。在 new 方法中,我们先检查 instance 属性:如果发现它尚未创建,则调用父类 new 方法进行创建;否则,直接将实例返回。

如果存在多个线程并发调用 Counter 创建实例对象的情况,将产生 竞争态 。举个例子,假设两个线程同时检查了 instance 属性,发现实例对象尚未创建,然后分头创建实例对象。

解决竞争态的手段也很简单,只需为 new 函数也加上一把锁,就像 inc 方法一样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Counter:

    lock = Lock()
    instance = None

    def __new__(cls, *args, **kwargs):
        with cls.lock:
            if cls.instance is None:
                cls.instance = super().__new__(cls, *args, **kwargs)

            return cls.instance

    def __init__(self):
        self.value = 0

    def inc(self):
        with self.lock:
            self.value += 1

注意到,例子将锁从实例属性,调整成类属性。这样一来,一把锁即可同时保护类方法( new )和实例方法( inc )。

metaclass

Python 对象模型中,类型对象的类型是一个特殊的类型对象,称为 元类 ( metaclass )。元类可以看作一个类型工厂,可以制造新的类型对象。

type 对象我们已经非常熟悉,它是 Python 内置的元类,提供了制造新类型的最基本能力。我们编写自定义类时,正是它在背后默默工作。

元类引入了一种强大的魔力,但它的 抽象性 却令许多人为之却步。为此,我准备了一个非常典型的例子,力求将元类的原理一次性讲透。为尽量降低阅读难度,我对例子作了最大程度的简化。

假设我们正在设计了一个用于实现插件的基类,它提供了 serve_forever 方法,具备根据指定时间间隔,循环执行 process 处理函数的基础能力:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import time

class BasePlugin:
    
    def __init__(self, interval):
        self.interval = interval
        
    def serve_forever(self):
        while True:
            self.process()
            time.sleep(self.interval)

Python 执行这段代码时,最终调用 type 对象完成 BasePlugin 类对象的创建,伪代码大致是这样的:

1
2
3
4
5
6
attrs = {
    '__init__': <function>,
    'serve_forever': <function>,
}

BasePlugin = type(name='BasePlugin', bases=(object,), attrs=attrs)

实际插件作为 BasePlugin 的子类来组织,子类从基类继承了 serve_forever 的能力,并在 process 方法中实现具体处理逻辑:

1
2
3
4
class BarPlugin(BasePlugin):

    def process(self):
        print('bar processing')

这时,确保子类 BarPlugin 实现了 process 方法就显得格外重要,只有满足这一点才算是一个合法的插件。

1
2
3
4
class FaultPlugin(BasePlugin):

    def run(self):
        print('xxxx')

FaultPlugin 就不是一个合法的插件,它没有实现 process 方法。然而,Python 默认无法发现这一点。

那么,有没有办法让 Python 在创建新类时,自动检查它是否实现了某个方法呢?点击“阅读原文”,获取更多详情!

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注:

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注: