使用Python元类实现单例模式

读者可能已经知道,在Python中实现工厂模式的最简单方法是为类添加一个工厂方法,例如:

import unittest
import threading


class SingletonBase(object):
    _instances = {}
    __lock = threading.Lock()

    @classmethod
    def factory(cls):
        if cls not in cls._instances:
            with cls.__lock:  # 双检锁
                if cls not in cls._instances:
                    cls._instances[cls] = cls()  # cls()生成实际子类的实例
        return cls._instances[cls]

    @classmethod
    def clear_instances(cls):
        cls._instances = {}


class SomeClass(SingletonBase):

    def __init__(self):
        # 警告: 必须调用factory方法获取单例,不能直接使用__init__初始化对象!
        print(f"called the __init__ method of class {type(self).__name__}")
        self._some_attr = 123

    @property
    def some_attr(self):
        print(f"getting property some_attr of class {type(self).__name__}")
        return self._some_attr

    @some_attr.setter
    def some_attr(self, value):
        print(f"setting property some_attr of class {type(self).__name__} to value {value}")
        self._some_attr = value


class TestSingleton(unittest.TestCase):

    def tearDown(self) -> None:
        SingletonBase.clear_instances()

    def test_some_class(self):
        instance_a = SomeClass.factory()
        instance_b = SomeClass.factory()
        self.assertIs(instance_a, instance_b)
        instance_a.some_attr = 500
        self.assertEqual(instance_b.some_attr, 500)
    # 运行输出:
    # called the __init__ method of class SomeClass
    # setting property some_attr of class SomeClass to value 500
    # getting property some_attr of class SomeClass

然而,由于python中不存在”构造函数”或者说是初始化函数的作用域限制,因此我们无法阻止用户直接使用SomeClass()经由__init__方法获得一个新的实例。这对多人配合开发的过程不太友好,尤其是在没有足够的文档和注释向合作者说明的情况下。如果说我们将SingletonBase类型放到别的文件中,继承关系再复杂一些,SomeClass的父类列表里也没有Singleton这个单词,那么出错的概率就更高了。

下面进入本文的正题,即使用元类实现更易于使用的单例模式:

import time
import unittest
import threading


class SingletonMeta(type):  # 1.元类继承自type类型

    __instances = {}
    __lock = threading.Lock()

    test_sleep = 0

    def __call__(cls, *args, **kwargs):  # 2.元类的实例方法的第一个参数是类型[class]
        if cls not in cls.__instances:
            if cls.test_sleep:
                # 为了方便测试多线程的情况,我们在这里允许线程暂停一段时间
                time.sleep(cls.test_sleep)
            with cls.__lock:  # 3.双检锁
                if cls not in cls.__instances:
                    cls.__instances[cls] = super().__call__(*args, **kwargs)
        return cls.__instances[cls]

    @classmethod
    def clear_instances(mcs):  # 4.元类的类方法的第一个参数是元类[metaclass]
        with mcs.__lock:
            mcs.__instances = {}


class SomeClass(object, metaclass=SingletonMeta):  # 5.声明元类

    def __init__(self):
        print(f"called the __init__ method of class {type(self).__name__}")
        self._some_attr = 123

    @property
    def some_attr(self):
        print(f"getting property some_attr of class {type(self).__name__}")
        return self._some_attr

    @some_attr.setter
    def some_attr(self, value):
        print(f"setting property some_attr of class {type(self).__name__} to value {value}")
        self._some_attr = value


class AnotherClass(SomeClass):
    pass

我们编写几个单元测试来测试一下上述代码的效果:

class TestSingletonMeta(unittest.TestCase):

    def tearDown(self) -> None:
        SingletonMeta.clear_instances()
        SingletonMeta.test_sleep = 0

    def test_some_class(self):
        instance_a = SomeClass()
        instance_b = SomeClass()
        self.assertIs(instance_a, instance_b)
        instance_a.some_attr = 500
        self.assertEqual(instance_b.some_attr, 500)
    # 运行输出:
    # called the __init__ method of class SomeClass
    # setting property some_attr of class SomeClass to value 500
    # getting property some_attr of class SomeClass

    def test_distinguish_derived_class(self):
        some_class_object = SomeClass()
        another_class_object = AnotherClass()
        self.assertIsNot(some_class_object, another_class_object)
        another_class_object_b = AnotherClass()
        self.assertIs(another_class_object, another_class_object_b)
    # 运行输出:
    # called the __init__ method of class SomeClass
    # called the __init__ method of class AnotherClass

    def test_concurrent(self):
        SingletonMeta.test_sleep = 0.5
        result = {}

        def thread_job(job_id):
            result[job_id] = SomeClass()

        threading.Thread(target=thread_job, args=["a"]).start()
        threading.Thread(target=thread_job, args=["b"]).start()

        time.sleep(0.7)
        self.assertIs(result["a"], result["b"])
    # 运行输出:
    # called the __init__ method of class SomeClass

为不熟悉相关技术的读者解释一下上述单例模式的实现。下述编号与代码注释中的编号对应。

  1. 在python中,类型也是对象,它们是“更高层次的类”的实例。我们称这种更高层次的类型为元类。我们自定义的元类继承自type这个最基本的元类,就如一般类型均继承自object一样。
  2. 由于元类的实例是类,因此元类的实例方法是类的类方法,第一个参数就如同一般的classmethod一样,是cls
  3. 这种做法称为双检锁,其优势是除第一次创建实例时以外,其余时候都不需要承担获得和释放锁的开销。
  4. 元类的类方法的第一个参数自然就是元类本身了,我们使用mcs作为metaclass的缩写
  5. 定义一个普通类型时,使用这种方法为其指派一个元类。该类型成为该元类的实例。就如同类实例(对象)从类上获得了实例方法一样,此时类型从元类上获得了我们自定义的__call__方法,因此在SomeClass()调用时执行我们定义的过程,实现单例模式。

基于元类的单例模式实现具有明显的优势:直接使用SomeClass()语法即可获得同一实例对象,该用法最为常见,且不存在其它模糊的、可能导致设计意图被绕过的用法。

看到这里,读者可能会提出问题:为何不使用__new__方法返回单个类实例,实现单例模式呢?我们来探究一下这种做法的弊端:

import unittest


class SomeClass(object):
    _instance = None

    def __new__(cls):
        if not cls._instance:  # 为简便起见就不写双检锁了
            obj = super().__new__(cls)
            obj._some_attr = 123
            cls._instance = obj
        return cls._instance

    def __init__(self):
        print(f"called the __init__ method of class {type(self).__name__}")
        # self._some_attr = 123
        # 不能在这里初始化任何属性,因为每次获得单例对象时__init__都会被调用,从而覆盖原来的值

    @property
    def some_attr(self):
        print(f"getting property some_attr of class {type(self).__name__}")
        return self._some_attr

    @some_attr.setter
    def some_attr(self, value):
        print(f"setting property some_attr of class {type(self).__name__} to value {value}")
        self._some_attr = value


class TestSingletonMeta(unittest.TestCase):

    def tearDown(self) -> None:
        SomeClass._instance = None

    def test_some_class(self):
        instance_a = SomeClass()
        instance_b = SomeClass()
        self.assertIs(instance_a, instance_b)
        instance_a.some_attr = 500
        self.assertEqual(instance_b.some_attr, 500)
    # 运行输出:
    # called the __init__ method of class SomeClass
    # called the __init__ method of class SomeClass
    # setting property some_attr of class SomeClass to value 500
    # getting property some_attr of class SomeClass

上面这段使用__new__方法实现的单例模式有两个显著的缺陷:一是由于__init__方法会被重复调用,因此只能在__new__方法中初始化属性,而这与我们一般的使用习惯是非常不同的。二是对象创建过程难以复用,需要给每个类型都定义__new__方法。看来虽然__new__方法能更复杂地操纵对象生成的过程,但是在实现单例模式的场景下并不十分实用。

读者还可能对元类的例子提出疑问:我们为何要大费周章在元类上重写__call__方法,是否可以在基类中复写object.__call__从而避免使用元类这么复杂的技术呢?答案是否定的,因为Python解释器在使用类名+括号生成类实例时使用了特殊的方法查找模式,因此原来的__call__方法无法被类定义修改,而只能由元类修改。来看以下“简单”的例子:

class SomeClass(object):
    @classmethod
    def __call__(cls):
        return 100


if __name__ == '__main__':
    print(SomeClass())  # 无法调用到我们自己定义的__call__方法
    print(SomeClass.__call__())
    # 运行输出:
    # <__main__.SomeClass object at 0x7f9d15f4cc10>
    # 100

上述程序的输出表明,SomeClass()方法通过其它逻辑找到了原来的__call__方法生成实例,而没有调用我们定义的返回整数的__call__方法。

发表评论

您的电子邮箱地址不会被公开。