读者可能已经知道,在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
为不熟悉相关技术的读者解释一下上述单例模式的实现。下述编号与代码注释中的编号对应。
- 在python中,类型也是对象,它们是“更高层次的类”的实例。我们称这种更高层次的类型为元类。我们自定义的元类继承自type这个最基本的元类,就如一般类型均继承自object一样。
- 由于元类的实例是类,因此元类的实例方法是类的类方法,第一个参数就如同一般的classmethod一样,是cls
- 这种做法称为双检锁,其优势是除第一次创建实例时以外,其余时候都不需要承担获得和释放锁的开销。
- 元类的类方法的第一个参数自然就是元类本身了,我们使用mcs作为metaclass的缩写
- 定义一个普通类型时,使用这种方法为其指派一个元类。该类型成为该元类的实例。就如同类实例(对象)从类上获得了实例方法一样,此时类型从元类上获得了我们自定义的__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__方法。