由于 Python 拥有灵活的动态语言特性和丰富的魔法方法,
再加上它强大的元类编程,单例模式(Singleton)在 Python 上的实现方式五花八门
本文主要讨论单例模式在 Python 上的各种实现
涉及到 Python 中元类编程、魔法方法和装饰器的进阶语法知识,并在最后深入讨论和总结。
关于单例模式
定义
Ensure a class has only one instance, and provide a global point of access to it.
- 某个类只能有一个实例
- 该类必须自行创建和管理这个实例
- 必须向整个系统提供这个实例
模式角色
只包含一个单例类,在 Python 下可以是一个装饰器(函数)
优点
- 提供对唯一实例的受控访问
- 节约系统资源,提高系统性能
- 允许可变数目的实例(多例模式)
缺点
- 缺少抽象层而难以扩展
- 单例类职责过重
适用场景
- 系统只需要一个实例对象(如windows的资源管理器、唯一序列号生成器、py中的None、某些UI窗口)
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例
分类
懒汉单例(常用):lazy singleton
在第一次被引用时,才将自己实例化。避免开始时占用系统资源,但是有多线程访问安全性问题。饿汉单例:eager singleton
在类被加载时就将自己实例化(静态初始化)。其优点是躲避了多线程访问的安全性问题,缺点是提前占用系统资源。
线程安全
懒汉单例在多线程环境下需要考虑线程安全的问题,因此需要使用threading.Lock
区别
这里需要注意的是,Python 中类的加载机制与 Java 的不太一样
前者动态后者静态,因此在 Python 中实现饿汉单例比较特殊
后面的变量名覆盖方法和import导入其实就是一种饿汉单例
关于 Python 中类的加载(导入)机制,可以看《流畅的Python》第21章元类编程中:导入时和运行时的比较,其中的一个理解计算时间的练习很有意思
扩展
多例模式,返回多个(有限个)实例对象,可以随机也可以按序
实现方式
在此之前,建议看我之前的一篇文章:魔法方法之 __new__()
与 __init__()
,因为接下来的各种实现方式很多都是通过重载 __new__()
和 __init__()
来实现的,否则你可能不知为何这么做就可以实现单例
1. 重载 __new__()
使用一个类的字典 _instances
来存放出现过的实例
采用公共的类字典的存储方式是考虑到Singleton可能会有子类,这样可以返回不同子类的单例
否则(使用一个类变量存储)一旦先实例化父类Singleton,则子类实例化时总返回父类的实例
该方法在每次生成实例时都会调用 __init__()
来重置实例属性
class Singleton:
_instances = {}
def __new__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__new__(cls, *args, **kwargs)
return cls._instances[cls]
def __init__(self):
print('call Singleton.__init__()')
class A(Singleton):
def __init__(self):
print('call A.__init__()')
print(Singleton() is Singleton())
print("* " * 10)
print(A() is A())
print("* " * 10)
print(type(A()))
输出:
call Singleton.__init__()
call Singleton.__init__()
True
* * * * * * * * * *
call A.__init__()
call A.__init__()
True
* * * * * * * * * *
call A.__init__()
<class '__main__.A'>
可以看到继承单例类的子类也能成为单例类,但注意到 __init__()
方法被调用了多次
这意味着每次实例化时虽然返回的是同一个实例对象
但初始化创建的属性会被重置和改变(传入不同的初始化参数),这往往不是我们想看到的
我们的要求更高了,返回第一次创建的实例还不够,我们还要求实例内部的环境变量也不能改变和重置
这就要求单例模式下 __init__()
只会被调用一次,即只在第一次创建实例被调用,如何做到呢?
2. 使用装饰器
使用一个字典来存放不同类对应的实例,因为一个装饰器可以用来装饰多个类
这样装饰器可以对不同的类生成不同的实例,对相同的类生成相同的实例
且该方法比第一种方法更优:只有第一次实例化调用了 __init__()
def singleton(cls):
instances = {} # 使用一个字典来存放不同类对应的实例,因为一个装饰器可以用来装饰多个类
def wrapper(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return wrapper
@singleton
class A:
def __init__(self):
print('call A.__init__')
@singleton
class B:
def __init__(self):
print('call B.__init__')
print(A() is B()) # False,对不同的类生成不同的实例
print(A() is A()) # True,对相同的类生成相同的实例
print(B() is B()) # True
输出:
call A.__init__
call B.__init__
False
True
True
可以看到,类 A 和 B 各自被调用了三次,但只有第一次才调用了各自的 __init__()
且装饰器 singleton 对不同的类生成不同的实例,对相同的类生成相同的实例。
但使用装饰器可能存在一个问题,即用装饰器修饰的单例类不能再有子类,否则在加载子类时会出错:
class C(A):
pass
输出:
TypeError Traceback (most recent call last)
<ipython-input-42-dcf34ebd5f77> in <module>()
----> 1 class C(A):
2 pass
TypeError: function() argument 1 must be code, not str
因为被装饰器装饰后的A已经不再是类了,而只是一个函数,类是无法去继承函数的
那如何做到既能只调用一次 __init__()
,有能让继承的子类为单例类呢?
3. 使用 metaclass
重载元类的 __init__()
和 __call__()
(或 __new__()
和 __call__()
)
其原理和重载 __new__()
一样,因为元类的 __call__()
首先就是调用类的 __new__()
来得到类的一个实例
因此我们可以在更早的时间点拦截,即在元类中提前检查是否含有单例
同时该方法和装饰器一样只调用一次 __init__()
class Singleton(type):
# def __new__(cls, name, bases, dict, **kwargs):
# cls._instance = None
# return super().__new__(cls, name, bases, dict, **kwargs)
def __init__(cls, name, bases, dict, **kwargs):
cls._instance = None
super().__init__(name, bases, dict)
def __call__(cls, *args, **kwargs):
if cls._instance is None: # 有就直接返回该实例对象,避免进入super().__call__()中重复调用类的__init__()
cls._instance = super().__call__(*args, **kwargs)
return cls._instance
class A(metaclass=Singleton): # 直接声明元类即可
def __init__(self):
print('call A.__init__')
class B(A): # 继承A,则其元类也是A的元类
def __init__(self):
print('call B.__init__')
print(A() is A())
print('* ' * 10 )
print(B() is B())
print('* ' * 10 )
print(type(B()))
输出:
call A.__init__
True
* * * * * * * * * *
call B.__init__
True
* * * * * * * * * *
<class '__main__.B'>
可见 B 继承 A 后也为单例类,且 __init__()
也只调用了一次
因为 __call__()
保证了不会进入super().__call__()
中重复调用类的 __init__()
4. 使用共享属性
首先强调一点,该方式实现的并不是严格的单例模式,实际上属于为Borg模式
表面上看,单例就是所有实例对象拥有相同的状态(属性)和行为(方法)
同一个类的所有实例本来就拥有相同的行为(方法)
只需要保证同一个类的所有实例具有相同的状态(属性)即可
所有实例共享属性的最简单最直接的方法就是将 __dict__
属性指向(引用)同一个字典
虽然这么做各个实例的id不一样,即内存地址不一样,但表现出来的行为和状态却是一样的
它们共享了同一套属性和方法,这让这些实例“看上去”像是单例的
注意到该方法也会在每次实例化时调用 __init__
重置实例属性
class Singleton:
_state = {}
def __new__(cls, *args, **kwargs):
inst = super().__new__(cls)
inst.__dict__ = cls._state
return inst
class MyClass(Singleton):
def __init__(self, v):
self.v = v
a = MyClass(1)
print(a.v)
b = MyClass(2)
print(a.v) # 这里a.v会被改为2
print(b.v)
print(a is b) # a和b的id其实不一样
print(a.__dict__ is b.__dict__) # 但a,b共享所有属性
输出:
1
2
2
False
True
如果想在比较 id 时表现出相等,也就是想更加相似单例模式(在充当字典key时),我们可以重载 __hash__()
和 __eq__()
:
class Singleton:
_state = {}
def __new__(cls, *args, **kwargs):
inst = super().__new__(cls)
inst.__dict__ = cls._state
return inst
def __hash__(self):
return 0
def __eq__(self,other):
return self.__dict__ is other.__dict__
class MyClass(Singleton):
pass
a = MyClass()
b = MyClass()
d = {}
d[a] = 1
d[b] = 2
print(d) # 输出{<__main__.MyClass object at 0x05668550>: 2}
字典 d 只有一个键值对,说明实例 a 和 b 在字典 key 上表现相等。
这里要说明一下: 字典在比较两个key是否相等时会使用 ==
符号,而 ==
符号会调用 __eq__()
方法
所以我们只需重载 __eq__()
来使得两个参与比较的单例 key 表现出相等即可
但还需要重载 __hash__()
,因为一个对象要成为字典的 key 就必须重载 __hash__()
来成为可散列对象
(默认的 __eq__()
方法在 x is y
并且 hash(x) == hash(y)
下才会返回True
is
会去调用 id()
内置方法判断两个对象的内存地址是否一样,这个无法重载
在共享属性的单例模式下我们两个实例的地址肯定是不一样的,这点没法修改
hash()
在默认情况下会返回一个随机值,每次返回结果几乎不一样, 但它回去调用 __hash__()
)
5. 使用 @classmethod
这种实现方式很牵强,通过实现一个类方法来返回实例,
用户通过调用该类方法来获取唯一的那个实例,但这不能阻止用户直接调用类(调用 __init__()
)来生成
在Java等静态语言中可以通过一个私有方法来将构造方法屏蔽起来不让用户去调用,但在Python中没有真正的私有方法
如果用户能做到严格按照类方法来获取实例,那么 __init__()
只会被调用一次
class Singleton():
_instance = None
def __init__(self):
print('call Singleton.__init__')
@classmethod
def getInstance(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = cls(*args, **kwargs)
return cls._instance
print(Singleton.getInstance() is Singleton.getInstance()) # 用户通过调用类方法来获取实例
print('* ' * 10)
print(Singleton() is Singleton()) # 但并不能阻止用户直接调用__init__()
输出:
call Singleton.__init__
True
* * * * * * * * * *
call Singleton.__init__
call Singleton.__init__
False
6. 使用变量名覆盖
最简单最巧妙的实现方式,得益于 Python 强大的动态语言特性和丰富的魔法方法
一开始就生成一个实例,并用一个与类名相同的变量名作为该实例的变量名
而我们在类的 __call__()
中总是返回自己,这样后面试图去生成新的实例时
其本质都是调用了 __call__()
来返回自己,十分巧妙,而且注意到这种实现方式是一种饿汉单例
同时该方法和装饰器一样只调用一次 __init__()
class Singleton:
def __init__(self):
print('call Singleton.__init__')
def __call__(self):
return self
class A(Singleton):
def __init__(self):
print('call A.__init__')
Singleton = Singleton() # 提前生成实例
A = A() # # 提前生成实例
# 下面才是正式调用,因此从某种程度来讲,这种实现方式属于饿汉单例
print("* " * 10)
print(Singleton() is Singleton() is Singleton)
print(A() is A())
print("* " * 10)
print(type(A()))
输出:
call Singleton.__init__
call A.__init__
* * * * * * * * * *
True
True
* * * * * * * * * *
<class '__main__.A'>
可以看到,该方法和装饰器一样只调用一次 __init__()
,且子类也能够继承而成为单例类,但要提前生成实例覆盖掉
注意不能在单例类 Singleton 覆盖后才声明子类,否则会出现意外的错误,因为子类继承的是一个实例了!
class Singleton:
def __init__(self):
print('call Singleton.__init__')
def __call__(self, *args):
print('call Singleton.__call__')
return self
Singleton = Singleton() # 在声明子类前覆盖
class A(Singleton):
def __init__(self):
print('call A.__init__')
A()
输出:
call Singleton.__init__
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-122-995c78abe87f> in <module>()
9 Singleton = Singleton() # 在声明子类前覆盖
10
---> 11 class A(Singleton):
12 def __init__(self):
13 print('call A.__init__')
TypeError: __init__() takes 1 positional argument but 4 were given
7. 使用 import 的模块导入
Python 的模块导入方式采用的就是天然的单例模式
通过 import,一个模块只会被导入并初始化一次,模块内的全局变量将会被绑定到模块上
因此我们只需把想要的单例提前在一个模块内作为全局变量实例化并删除该类,然后在别的模块中导入即可:
# a.py:
class A:
pass
a = A()
del A
# b.py:
from a import a
这种实现方式属于饿汉模式,是线程安全的,并且import本身的实现机制也是线程安全的
其典型的例子就是标准库中的logging模块,它在被导入时会生成一个全局的 Logger.manager 单例作为 root
然后每次通过 logging.getLogger(name) 调用时总是通过 root.loggerDict 获取同一个 Logger
通过查看源码可以了解更多实现细节
8. 线程安全
到目前为止一共讨论了七种实现单例模式的方法,使用元类的实现方式看起来不错
但除了变量名覆盖
和import模块导入
,其他五种实现方式都没有考虑线程安全的问题
因此这里以重载 __new__()
为例,加入Lock来实现线程安全的单例模式,这里实现一个装饰器 synchronized
import threading
def synchronized(func):
_lock = threading.Lock()
def lock_func(*args, **kwargs):
with _lock:
return func(*args, **kwargs)
return lock_func
class SyncSingleton:
_instances = {}
@synchronized # 注意到装饰器是在载入时执行的,所以装饰器本身是线程安全的
def __new__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__new__(cls, *args, **kwargs)
return cls._instances[cls]
把装饰器展开来就是:
class Singleton:
_instances = {}
_lock = threading.Lock()
def __new__(cls, *args, **kwargs):
with cls._lock:
if cls not in cls._instances:
cls._instances[cls] = super().__new__(cls, *args, **kwargs)
return cls._instances[cls]
注意到线程每次都要阻塞在 with cls._lock
,其实在第一个实例生成时cls的实例就已经在_instances
字典中了
所以我们可以在 with cls._lock
外面再加一层检查判断即可,避免每次都去争取锁而进行不必要的阻塞
# 双重检查的锁机制
class Singleton:
_instances = {}
_lock = threading.Lock()
def __new__(cls, *args, **kwargs):
if cls not in cls._instances: # 外层提高性能
with cls._lock:
if cls not in cls._instances: # 内层是核心逻辑
cls._instances[cls] = super().__new__(cls, *args, **kwargs)
return cls._instances[cls]
以上完整的代码和多线程测试代码见Github
9. 总感觉还有什么没弄明白
是的,往往有时候我们看完一篇技术文章就处于“知其然而不知其所以然”的状态,看似懂了,明白如何用了
但其实背后更深层的原理我们并不了解
如果意识到该问题所在,自然就会去深挖,怕的是走马观花,不能静下心来多问自己几个为什么。
为什么重载 __new__()
就能实现单例?
如果在此之前你已经仔细阅读过我的这篇文章:魔法方法之 __new__()
与 __init__()
就不会问这样的问题了。
为什么在 metaclass 中重载元类的 __init__()
和 __call__()
(或 __new__()
和 __call__()
)就能实现单例?
这就需要我们搞清楚一个问题:
一个类从被加载进全局环境,到真正被调用运行并返回一个实例,这中间经历了什么?经历了哪些魔法方法?
关于这个问题可以参考《流畅的Python》第21章元类编程相关的讲解,十分精彩
如果看的时候很吃力,不妨先看看我之前写过的另外一篇文章:Python 中 type 与 object 的关系
这里我用自己的代码和注释来展现这个过程:
class ABC:
print('ABC') # ABC被导入时执行,而__new__和__init__是在被调用时才执行
class CBA:
print('CBA')# 嵌套类的定义体也会在被导入时执行
def __new__(cls):
print('new')
return super().__new__(cls)
def __init__(self):
print('init')
# 见《流畅的python》P543--导入时和运行时的比较
# 类的定义体也属于顶层代码
# 类被导入时会嵌套地进入定义体,而函数(方法)不会,它只是绑定而已
# 导入类时这么做的原因是它要绑定类的属性和方法
print('* ' * 10)
a = ABC() # 如果把这一行注释掉,那么print('ABC')和print('CBA')一样会被执行,而__new__和__init__就不会了
输出:
ABC
CBA
* * * * * * * * * *
new
init
上面是一个关于类加载和运行的例子,下面来看看其内部经过的魔法函数:
class Meta(type):
print('[1]Meta start')
def __new__(cls, name, bases, dict, **kwargs): # 在加载时调用,且在类加载完后调用
# 这里的cls是Meta本身
print('[3]Meta new')
return super().__new__(cls, name, bases, dict, **kwargs)
def __init__(cls, name, bases, dict): # 在加载时调用,且在上面的__new__()后调用
# 这里是cls,不是self了,因为上面__new__()返回的是一个类,语义上应该是cls,写self也没错
print('[4]Meta init')
super().__init__(name, bases, dict) # 父类type参数中没有cls
def __call__(cls, *args, **kwargs): # 在类生成实例时调用,其调用了以它为元类的类的__new__()和__init__()
print('[5]Meta call')
return super().__call__(cls, *args, **kwargs) # 在调用完类的__new__()和__init__()后返回该类的实例
# 如果自己实现的话大致逻辑如下:
# self = cls.__new__(cls, *args, **kwargs)
# if isinstance(self, cls): # __new__() 返回的是cls的实例才调用该实例的__init__()
# self.__init__(*args, **kwargs)
# return self
class A(metaclass=Meta):
print('[2]A start')
def __new__(cls, *args, **kwargs): # 在元类的__call__()中被调用
print('[6]A new')
return super().__new__(cls)
def __init__(self, *args, **kwargs): # 在元类的__call__()中被调用
print('[7]A init')
def __call__(self, *args, **kwargs): # 类的实例作为函数调用时调用该魔法方法
print('[8]A call')
return 1
print('* '*10)
a = A() # 调用了A的元类的__call__()
print('* '*10)
aa = a() # 调用了A的__call__()
print('* '*10)
print(aa)
输出:
[1]Meta start
[2]A start
[3]Meat new
[4]Meta init
* * * * * * * * * *
[5]Meta call
[6]A new
[7]A init
* * * * * * * * * *
[8]A call
* * * * * * * * * *
1
相信到这里你就会明白了:
一个类的加载和运行是两个分开的阶段,先加载,再根据需要运行
类中的顶层代码(方法之外的定义体)在加载时执行,而方法内的代码在调用时才执行
这就是输出中 1 和 2 最先出现的原因
但由于元类是类的类,因此以它为元类的类在加载完后会紧接着执行它指定的元类的 __new__()
和 __init__()
这也是输出中 3 和 4 紧跟着出现在 1 和 2 后面的原因
至于输出中的 5、6、7,反映的就是我们调用 a = A() 时经过的魔法方法:
先是元类 Meta 的 __call__()
,然后是类 A 的__new__()
和 __init__()
注意到,在类生成实例时,元类的 __call__()
内部会调用以它为元类的类的 __new__()
和 __init__()
这是根据魔法方法之 __new__()
与 __init__()
中讨论的内容模仿 type 而实现的简单逻辑
因此我们为了可以控制一个类生成实例的行为,我们可以既可以简单地重载该类的 __new__()
也可以自己实现一个元类并重载它的 __init__()
和 __call__()
或 __new__()
和 __call__()
然后该类只需指明它的元类是这个自定义的元类而不是默认的 type 即可(默认情况下就是type.__call__(A)
):
class Meta(type):
def __call__(cls, *args, **kwargs):
print('call Meta.__call__() ')
return super().__call__()
class A(metaclass=Meta):
pass
a = A() # 调用的是Meta.__call__()
print(isinstance(a, A))
print('* ' * 10)
b = type.__call__(A) # 直接调用type.__call__(),绕过Meta.__call__()
print(isinstance(b ,A))
输出:
call Meta.__call__()
True
* * * * * * * * * *
True
最后输出中的 [8],是为了更好地理解 __call__()
:类的括号调用会去调用元类的__call__()
作为返回
那么实例的括号调用就会去调用类的__call__()
作为返回
其本质是一样的,__call__()
总是在使用括号调用时被调用,通过对它的重载我们可以自定义这个返回的对象
更多探索
综合前面提到的那两篇文章和本章内容,会看到:
type 其实负责对 Python 中类和元类在加载完成后的处理和调用时 __new__()
和 __init__()
的调度(当然其工作不止如此)
这从上面我们模仿 type.__call__
的逻辑实现就可以看出
我们可以编写自己的元类来自定义加载后的处理和调用时的调度过程,但由于元类本身也是type的实例
因此元类也要受 type 的处理和调度
我们还注意到默认情况下在子类的 __new__()
中总是会去调用父类的 __new__()
而父类的 __new__()
也是这么做,最终我们的 cls 参数会一路往上传递来到 object
而我们知道 object 没有父类,这从 object.__bases__
返回空元组可知
因此 cls 来到 object 就不会再往上传递了,这时候就由解释器(Cython) 内部来实现并生成 cls 的实例并返回
所以这就解释了为什么 Python 中所有的实例都是 object 的实例
而 Dashed Arrow Up Rule 的本质其实就是 cls 在 __new__()
中被往上传递
那我们能否自定义一个类似 object 的类,它直接生成传递上来的cls的实例并返回呢?
这样做可以打断这条继承链,让 object 不再是所有实例的类。。。只能说想的美:
class obj:
def __new__(cls):
print('call obj.__new__()')
return cls() # 没有往上传递了,直接生成实例返回
class A(obj):
def __new__(cls, *args, **kwargs):
print('call A.__new__()')
return super().__new__(cls)
a = A()
输出:
call A.__new__()
call obj.__new__()
call A.__new__()
call obj.__new__()
call A.__new__()
call obj.__new__()
# 中间省略n行
---------------------------------------------------------------------------
RecursionError Traceback (most recent call last)
<ipython-input-41-698ca043359a> in <module>()
9 return super().__new__(cls)
10
---> 11 a = A()
<ipython-input-41-698ca043359a> in __new__(cls, *args, **kwargs)
7 def __new__(cls, *args, **kwargs):
8 print('call A.__new__()')
----> 9 return super().__new__(cls)
10
11 a = A()
<ipython-input-41-698ca043359a> in __new__(cls)
2 def __new__(cls):
3 print('call obj.__new__()')
----> 4 return cls()
5
6 class A(obj):
... last 2 frames repeated, from the frame below ...
<ipython-input-41-698ca043359a> in __new__(cls, *args, **kwargs)
7 def __new__(cls, *args, **kwargs):
8 print('call A.__new__()')
----> 9 return super().__new__(cls)
10
11 a = A()
RecursionError: maximum recursion depth exceeded
Boom!直接爆炸溢出,因为父类的 return cls() 明显会再次调用子类的 __new__()
,造成无穷递归。。。
所以,还是乖乖 super().__new__(cls, *args, **kwargs )
吧!
在 __new__()
中我们可以向父类传递不是本类的cls,从而生成其他类的实例,甚至可以拐个弯:
class A:
def __new__(cls, *args, **kwargs):
print('call A.__new__()')
return B.__new__(cls, *args, **kwargs) # 拐了个弯,返回的其实是A的实例
def __init__(self, *args, **kwargs):
print('call A.__init__()')
class B:
def __new__(cls, *args, **kwargs):
print('call B.__new__()')
print(cls) # 来到B.__new__()的cls其实是A而不是B
return object.__new__(cls, *args, **kwargs)
a = A()
输出:
call A.__new__()
call B.__new__()
<class '__main__.A'>
call A.__init__() # 能自动调用A.__init__()
但无论如何最终一定会来到object,由object负责生成cls的实例
因此最终:
对于type,其主“外”,我们使用 metaclass 进行拦截,但最终还得回到type
对于object,其主“内”,我们使用 __new__()
进行拦截,但最终还是来到object
类内部生成的实例会经过外部元类的 __call__()
因此我们的单例模式既可以在类(内部)使用 __new__()
进行拦截,也可以在元类(外部)使用__call__()
进行拦截
参考资料
- 《设计模式》刘伟著
- 《流畅的Python》第21章:元类编程
- StackOverflow: creating-a-singleton-in-python