文章目录
单例模式是一种创建型设计模式,其目的是为了保证一个类仅有一个实例,并提供一个访问它的全局访问点,这么做有时是为了限制对共享资源的并发访问。
关于这个模式,我不打算像前面几个创建型模式逐步讲为什么使用,而是关注怎么实现单例
的目的。
单例模式的实现
单例模式有两种实现方式:
- Module-level Singleton
- Classic Singleton
Module-level Singleton
通过模块文件的方式实现,一个模块本身就是一个单例。
Modules are "singletons" in Python because import only creates a single copy of each module; subsequent imports of the same name keep returning the same module object.
首先我们的文件结构如下:
.
├── test0.py
├── test2.py
└── utils.py
在 utils.py
中的代码:
python
print("import utils:")
class Man:
common = {}
def __init__(self):
self.shared = {}
man = Man()
shared_value = "hello"
在 test0.py
中的代码:
python
import utils
print(utils.shared_value)
utils.shared_value += " add by test0 module"
print(id(utils))
print(id(utils.man))
在 test2.py
中的代码:
python
import utils
print("*"*10)
print(utils.shared_value)
print(id(utils))
print(id(utils.man))
此时在终端中调试
>>> import test0
import utils:
hello
2264080776976
2264081025424
>>> import test2
**********
hello add by test0 module
2264080776976
2264081025424
可以看到在同一个进程中, test0
和 test2
都在代码中执行过import utils
,对比输出后可以得到下面的观察结果:
- 该模块只被执行一次(这可以从
import test0
的输出import utils
获知) - 两个代码中出现的
utils
的id
是一致的,这也意味着调用它们的属性也会是一致的,比如man
这个Man
实例 - 在
test0
中执行utils.shared_value
修改影响到test2
中的utils.shared_value
而这就意味着我们用模块文件的形式实现了单例。
Classic Singleton
这是经典的单例实现方式,通过这种方式实现的类只会在没有其他实例存在时,才会创建一个实例,否则它只会返回已经创建的类实例。
相信大家都对 __init__
很熟悉,但很少接触到 __new__
, 而这种单例的实现就是基于这个对这个类型函数的修改。
首先要明白一个类进行实例化时会经过下面的调用流程:
调用 new 创建实例 调用 init 对实例进行初始化
而对 __new__
的实现中主要两种形式:
- 定义一个类级别的字典变量,每次生成实例前判断字典中是否有值
- 定义一个类级别的属性,每次生成实例前判断这个属性是否为空或者是否存在
至于再将这种形式实现成装饰器
还是单例类
也不过是额外发挥罢了。
python
# 利用 object instance
class SingletonClass(object):
def __new__(cls):
if not hasattr(cls, "instance"):
cls.instance = super(SingletonClass, cls).__new__(cls)
return cls.instance
# 利用字典变量 instance
class SingletonClassVersion2(object):
instance = {}
def __new__(cls):
if "singleton" not in cls.instance:
cls.instance["singleton"] = super(SingletonClass, cls).__new__(cls)
return cls.instance["singleton"]
singleton = SingletonClass()
new_singleton = SingletonClass()
print(singleton is new_singleton)
print(id(singleton))
print(id(new_singleton))
但这种实现方式有一个问题:即它虽然阻止了类每一次调用__new__
新建一个实例,但是它每次都会触发__init__
,这意味着定义在__init__
函数中的初始化操作会被重复执行,覆盖掉实例变量之前对实例变量的操作,因此共享数据需要保存在类变量中。
解决这个问题的思路就是修改__init__
方法:
python
class SingletonClass(object):
instance = None
initialized = False
def __new__(cls):
if not cls.instance:
cls.instance = super(SingletonClass, cls).__new__(cls)
return cls.instance
def __init__(self):
if not self.initialized:
# 这里放置初始化逻辑
self.initialized = True
print("Initializing SingletonClass instance")
单例模式实现的优化
线程安全问题
上面的 classic singleton
实现方式在并发程序中可能会出现多个线程同时访问类进行实例化时创建两个实例。为了确保同一时刻只有一个线程可以调用 __new__
,需要增加锁机制和重复检查。
python
from threading import Lock
class SingletonClass(object):
_lock = Lock()
def __new__(cls):
if not hasattr(cls, "instance"):
with cls._lock:
if not hasattr(cls, "instance"):
cls.instance = super(SingletonClass, cls).__new__(cls)
return cls.instance
可以看到程序中不但增加锁,还进行 double-check,这样做的原因可以看下面这张流程图,可以看到在 thread2 试图创建实例
时,如果不再次进行实例检查(此时thread1 已经创建实例)就会创建又一个实例。
No thread1释放锁 thread1 thread2 instance exists? 争夺锁资源 thread1 抢到 thread2 阻塞 创建实例 thread2 获取到锁资源 试图创建实例
违反单一职责原则
classic singleton
的实现方式将维护一个实例
的职责和类本身的功能
职责组合在一起,这不利于维护。优化这个问题的两种思路:
- 作为类装饰器实现
- 作为 metaclass 使用
类装饰器的实现
python
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Wizard:
def __init__(self, name):
self.name = name
这种方式的好处在于使用灵活
并且清晰
(只要使用该装饰器就知道这个类是单例)
metaclass方式实现
python
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(SingletonMeta, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class Wizard(metaclass=SingletonMeta):
def __init__(self, name):
self.name = name
单例模式的应用
- 数据库连接的
connection
管理, 一般会使用一个连接池实例(单例模式),降低每次都要新建连接的开销 - 常见的
Logger
实例,项目中的代码使用一个共同的日志实例 - 管理配置的实例