10.实现单例模式
1. 什么是单例模式
——一个实例
单例模式(Singleton Pattern)
一种常用的软件设计模式,该模式的主要目的是确保某一个类只有一个实例存在。- 当你希望在整个系统中,某个类只能出现一个实例时,单例对象就能派上用场。
单例模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
关键实现思想:
- 保证构造函数是私有的(比如 java、C++、C#)语言,像 python 这种没办法构造私有函数的怎么办呢?后面会讲到它灵活的实现。
- 第一次创建类的对象的时候判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
- 那么后续再次创建该类的实例的时候,因为已经创建过一次了,就不能再创建新的实例了,否则就不是单例啦,直接返回前面返回的实例即可。
提示
单例模式根本实现原理是在 __call__
、__new__
、__init__
方法中添加判断,如果已经创建过实例,则直接返回之前创建的实例,否则创建一个新的实例。
- 元类
__call__
方法:实例化过程中最早调用的方法, 它控制了__new__
和__init__
的执行。 __new__
方法:是第二个调用的方法, 影响创建实例的具体过程.__init__
方法:是第三个调用的方法, 影响示例的初始化过程.
2. 为什么要实现单例模式呢?
2.1 常见的单例模式:
- 比如,某个服务器程序的配置信息存放在一个文件中,客户端通过一个 AppConfig 的类来读取配置文件的信息。如果在程序运行期间,有很多地方都需要使用配置文件的内容,也就是说,很多地方都需要创建 AppConfig 对象的实例,这就导致系统中存在多个 AppConfig 的实例对象,而这样会严重浪费内存资源,尤其是在配置文件内容很多的情况下。事实上,类似 AppConfig 这样的类,我们希望在程序运行期间只存在一个实例对象。
- 当有同步需要的时候,可以通过一个实例来进行同步控制,比如对某个共享文件(如日志文件)的控制,对计数器的同步控制等,这种情况下由于只有一个实例,所以不用担心同步问题。
- Python 的 logger 就是一个单例模式,用以日志记录;Windows 的资源管理器是一个单例模式;线程池,数据库连接池等资源池一般也用单例模式;网站计数器等等,这些都是单例模式。
2.2 需要使用单例模式的情景
:——这个很重要哦
当每个实例都会占用资源,而且实例初始化会影响性能,这个时候就可以考虑使用单例模式,它给我们带来的好处是只有一个实例占用资源,并且只需初始化一次;归纳为以下几条:
- 要求生产唯一序列号。
- WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
- 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
2.3 优缺点:
优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 避免对资源的多重占用(比如写文件操作)。
缺点:
- 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
3. 单例模式的实现
3.1 使用 模块
相关信息
模块天然就是单例的,模块在第一次导入时,会生成 .pyc 文件,当第二次导入时,就会直接加载 .pyc 文件,而不会再次执行模块代码。
即使在多线程的环境下,也只会导入一次,因此,我们只需把相关的函数和数据定义在一个模块中,就可以获得一个单例对象了。
class Singleton():
def __init__(self, name):
self.name = name
def do_something(self):
pass
singleton = Singleton('模块单例')
from my_singleton import singleton
3.2 使用装饰器
相关信息
创建 slt_2 对象时,instance 字典中已经存在 SingletonTest 这个 key,因此直接返回了第一次创建的对象,slt_1 和 slt_2 是同一个对象。
在多线程环境下,多个线程同时判断 cls 是否在 instance 字典中,得到的返回结果都是 False, 于是这些线程都会去创建对象,为了避免这种情况,加上一把 RLock 锁
from threading import RLock
single_lock = RLock()
def Singleton(cls):
instance = {}
def _singleton_wrapper(*args, **kargs):
with single_lock:
if cls not in instance:
instance[cls] = cls(*args, **kargs)
return instance[cls]
return _singleton_wrapper
@Singleton
class SingletonTest(object):
def __init__(self, name):
self.name = name
slt_1 = SingletonTest('第1次创建')
print(slt_1.name) # 第1次创建
slt_2 = SingletonTest('第2次创建')
print(slt_1.name, slt_2.name) # 第1次创建 第1次创建
print(slt_1 is slt_2) # True
from threading import RLock
single_lock = RLock()
def Singleton(cls):
instance = {}
def _singleton_wrapper(*args, **kargs):
with single_lock:
if cls not in instance:
instance[cls] = cls(*args, **kargs)
return instance[cls]
return _singleton_wrapper
@Singleton
class SingletonTest(object):
def __init__(self, name):
self.name = name
slt_1 = SingletonTest('第1次创建')
print(slt_1.name) # 第1次创建
slt_2 = SingletonTest('第2次创建')
print(slt_1.name, slt_2.name) # 第1次创建 第1次创建
print(slt_1 is slt_2) # True
3.3 使用类
相关信息
instance 方法会先检查是否存在类属性_instance, 如果不存在,则创建对象,并返回。
这个设计虽然实现了单例模式,但在多线程环境下不安全,多个线程同时检查 Singleton 类是否拥有_instance 属性,得到的结果是否 False, 则这些线程都会执行对象的创建工作,最后创建出来的对象才是最终的对象,为了在多线程环境下保证数据安全,在需要并发枷锁的地方加上 RLock 锁
from threading import RLock
class Singleton(object):
single_lock = RLock()
def __init__(self, name):
self.name = name
@classmethod
def instance(cls, *args, **kwargs):
with Singleton.single_lock:
if not hasattr(Singleton, "_instance"):
Singleton._instance = Singleton(*args, **kwargs)
return Singleton._instance
from threading import RLock
class Singleton(object):
single_lock = RLock()
def __init__(self, name):
self.name = name
@classmethod
def instance(cls, *args, **kwargs):
with Singleton.single_lock:
if not hasattr(Singleton, "_instance"):
Singleton._instance = Singleton(*args, **kwargs)
return Singleton._instance
3.4 基于 __new__
方法实现
__new__方法是构造函数,是真正的用来创建对象的,那么在创建对象的时候进行控制,不是更方便么
class Singleton(object):
_instance = None
_lock = RLock()
_initialized = False # 新增一个类属性,用于标记是否已经初始化
def __init__(self, name):
if not self._initialized: # 只有当_initialized为False时,才执行初始化操作
self.cache = cachetools.LRUCache(maxsize)
logger.info(f"RAM 缓存初始化成功, maxsize: {maxsize}")
self._initialized = True # 初始化完成后,_initialized设置为True,避免再次初始化
self.name = name
def __new__(cls, *args, **kwargs):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super(Singleton, cls).__new__(cls)
return cls._instance
single_1 = Singleton('第1次创建')
single_2 = Singleton('第2次创建')
print(single_1.name, single_2.name) # 第2次创建 第2次创建
print(single_1 is single_2) # True
说明
这种方法同样实现了多线程环境下的安全的单例模式,但有一个小小的问题,第二次创建对象时,虽然返回了单例,但是修改了单例的name属性.
因为该方法会多次调用__new__
方法,虽然每次返回的都是同一个对象,但是会立即调用__init__
方法,这样就修改了name
属性,因此需要在 __init__
中添加一个判断, 一旦发现name属性已经被初始化,就不在执行初始化的代码。
3.5 使用元类
from threading import RLock
class SingletonType(type):
single_lock = RLock()
def __call__(cls, *args, **kwargs): # 创建cls的对象时候调用
with SingletonType.single_lock:
if not hasattr(cls, "_instance"):
cls._instance = super(SingletonType, cls).__call__(*args, **kwargs) # 创建cls的对象
return cls._instance
class Singleton(metaclass=SingletonType):
def __init__(self, name):
self.name = name
single_1 = Singleton('第1次创建')
single_2 = Singleton('第2次创建')
print(single_1.name, single_2.name) # 第1次创建 第1次创建
print(single_1 is single_2) # True
说明
class Singleton(metaclass=SingletonType)
这行代码定义了一个类,这个类是元类 SingletonType 的实例,是元类 SingletonType 的__new__
构造出来的,Singleton
是实例,那么Singleton
('第1次创建')就是在调用元类 SingletonType 的__call__
方法,__call__
方法可以让类的实例像函数一样去调用。
在__call__
方法里,cls 就是类 Singleton,为了创建对象,使用 super 来调用 __call__
方法,而不能直接写成 cls(*args, **kwargs)
, 这样等于又把 SingletonType 的 __call__
方法调用了一次,形成了死循环。