关于单例模式
单例模式提供了这样的一个机制,确保类有且只有一个特定类型的对象,并提供全局访问点。所以单例模式适用于以下情况:日志记录与数据库操作、打印机后台处理程序等等。因为这些程序运行过程中只能生成一个实例,用来避免对同一资源产生相互冲突的请求。
单例模式的特点:
- 确保有且只有一个对象被创建。
- 为对象提供一个访问点,以使程序可以全局访问该对象。
- 控制共享资源的并行访问。
Singleton |
---|
-instance:Singleton |
-Singleton() +instance():Singleton |
实现单例模式的一个简单方法,使构造函数私有化,并创建一个静态方法来完成对象的初始化。这样,对象将在第一次调用时创建,在此后,这个类将返回同一个对象。(在python中它无法创建私有的构造函数。)
单例的实现
下面让我们看看Python中如何来实现单例模式:
单例模式实现的两件事:
1.只允许Singleton类生成一个实例。
2.如果已经有了一个实例,它将会重复提供同一个对象。
经典单例模式
例:
class Singleton(object):
def __new__(cls):
if not hasattr(cls, 'instance'):
cls.instance = super(Singleton, cls).__new__(cls)
return cls.instance
a = Singleton()
print('object created', a)
b = Singleton()
print('创建对象:', b)
输出结果:
在上面的代码,我们通过覆盖__new__()方法(python用于实例化对象的特殊方法)来控制对象的创建。对象a就是由__new__()方法创建的,但是会在创建之前该方法会检查对象是否已经存在。
方法hasattr(python的内置函数,用来检查对象是否拥有某个属性。)用于查看对象cls是否具有属性instance,这个属性的作用就是检查该类是否已经生成一个对象。所以当b被请求时,hasattr()发现对象已经存在,所以,对象b会被分配已有的对象实例(0x0000029CED70DDF0)
单例模式中的懒汉式实例化
在导入模块时,我们可能无意中创建一个对象,但当时根本用不到它,懒汉式实例化就能够确保在使用时才创建对象,所以懒汉式实例化是一种节约资源并仅在需要时才创建的它们的方式。
例:
class Singleton:
__instance = None
def __init__(self):
if not Singleton.__instance:
print(" __init__method called..")
else:
print("instance already created:")
@classmethod
def getInstance(cls):
if not cls.__instance:
cls.__instance = Singleton()
return cls.__instance
s = Singleton() # 在这里类已经初始化但是对象没有被创建
print("Object created", Singleton.getInstance()) # 创建对象
s1 = Singleton() # 对象已经创建
输出结果:
在上面代码中,执行s = Singleton()时,它会调用__init__()方法,但是没有新对象被创建,实际的对象创建发生在Singleton.getInstance()的时候,这就是懒汉式实例化的实现。
模块级别的单例模式
在python中,所有模块都是单例,这是python的导入行为决定的。
- 检查一个python模块是否已经导入。
- 如果已经导入,则返回该模块的对象,如果还没导入,则导入该模块,并实例化。
- 所以,当模块被导入时,它就会被初始化。然而,当同一个模块被再次导入时,它不会被再次初始化,因为单例模式只能有一个对象,所以,它只会返回同一个对象。
Monostate(单态)单例模式
根据AlexMartelli的说法,通常程序员需要的是让实例共享相同的状态。他建议开发人员应该关注状态和行为,而不是同一性。由于该概念基于所有对象共享相同状态,所以它也被称为Monostate(单态)模式。
Monostate模式可以在python中轻松实现。
class Borg:
__shared_state = {"1": "2"}
def __init__(self):
self.x = 1
self.__dict__ = self.__shared_state
pass
b = Borg()
b1 = Borg()
print("Borg Object 'b':", b)
print("Borg Object 'b1':", b1)
b.x = 4
print("Object State 'b':", b.__dict__)
print("Object State 'b1':", b1.__dict__)
输出结果如图:
在这里可以看出b和b1明显是两个不同实例对象。在上面代码中我们将类的变量 __shared_state赋给了变量_dict_(它是python中的一个特殊变量)。python使用__dict__存储一个类所有对象的状态。在上面代码中我们创建了两个实例(b和b1)。这是两个不同的对象,在这一点上与我们的单例模式有很大的不同,后者只能生成一个对象,然而,对象的状态,也就是我们b.__dict__和b1.__dict__是相同的。所以就算对象b的对象变量x发生了变化,这个变化也会复制到所有对象共享的__dict__变量,即b1变量x的值也会从1变为4。
我们还可以通过修改__new__方法本身来实现Borg模式。我们知道,__new__方法是用来创建对象的实例的。
例:
class Borg(object):
_shared_state = {}
def __new__(cls, *args, **kwargs):
obj = super(Borg, cls).__new__(cls, *args, **kwargs)
obj.__dict__ = cls._shared_state
return obj
下面代码将更好的帮我们结束基于元类的单例实现。
class MetaSingleton(type):
_instance = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instance:
cls._instance[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs)
return cls._instance[cls]
class Loggger(metaclass=MetaSingleton):
pass
a = Loggger()
b = Loggger()
print(a, b)
关于元类的详解在这里面深入浅出元类详解。
单例模式I
作为一个实际的用例,我们将通过一个数据库应用程序来展示单例的应用。这里不妨以需要对数据库进行多种读取和写入操作的云服务为例进行讲解。完整的云服务被分为多个服务,每个服务执行不同的数据库操作。针对UI(web程序)上的操作将调用API,最终产生响应的DB操作。
很明显,跨不同服务的共享资源是数据库本身。因此,为了更好地设计云服务,必须注意以下几点:
数据库中的操作的一致性,即一个操作不应与其他操作冲突。
优化数据库的各种操作,以提高内存和CPU的利用率。
例:
import sqlite3
class MetaSingleton(type):
_instance = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instance:
cls._instance[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs)
return cls._instance[cls]
class Database(metaclass=MetaSingleton):
connection = None
def connect(self):
if self.connection is None:
self.connection = sqlite3.connect('db.sqlite3')
self.cursorobj = self.connection.cursor()
return self.cursorobj
db1 = Database().connect()
db2 = Database().connect()
print('数据库操作对象db1:', db1)
print('数据库操作对象db2:', db2)
输出结果:
结论:
- 我们使用MetaSingleton为名创建了一个元类。因为python的特殊方法__call__可以通过元类创建单例。
- 数据库类由MetaSingleton类装饰后,其行为就会表现为单例。因此,当数据库类被实例化时,它只会创建一个对象。
- 当web应用程序对数据库执行某些操作时,它会多次实例化数据库类,但只会创建一个对象,因为只有一个对象所以对数据库的调用是同步的。这样还能节约系统资源。
- 但是如果这个例子放在集群化情景时,即多个web应用程序共享单个数据库,单例在这时就不太好使了。因为每增加一个web应用程序,就要新建一个单例,添加一个新对象来查询数据库,这样就会导致数据库无法同步,还会浪费大量资源。这时,使用数据库连接池要比单例好太多了。
单例模式II:
让我们考虑另外一种情况,即为基础设施提供运行状况监控服务。我们创建了HealthCheck类,它作为单例实现。我们还要维护一个被监控的服务器列表。当一个服务器从列表中删除时,监控软件应该察觉到这一点,并从被监控的服务器列表中将其删除。
例:
class HealthCheck:
_instance = None
def __new__(cls, *args, **kwargs):
if not HealthCheck._instance:
HealthCheck._instance = super(HealthCheck, cls).__new__(cls, *args, **kwargs)
return HealthCheck._instance
def __init__(self):
self._servers = []
def addServer(self):
self._servers.append("Server1")
self._servers.append("Server2")
self._servers.append("Server3")
self._servers.append("Server4")
def changerServer(self):
print('sss', self._servers)
self._servers.pop()
self._servers.append('Server5')
a = HealthCheck()
b = HealthCheck()
a.addServer()
print('这是服务器的安全健康检查1111!!!')
for i in range(4):
print("检测:", a._servers[i])
b.changerServer()
print('这是服务器的安全健康检查2222!!!')
for i in range(4):
print('检测:',b._servers[i])
输出:
在上面代码中,我们先使用addServer()方法将服务器添加到基础设施中,以进行状况检查。首先通过迭代对这些服务器的运行状况进行检查。之后,changeServer方法会删除最后一个服务器,并向计划进行运行状况检查的基础设施添加一个新服务器。因此,当运行状况检查进行第二次迭代时,它会使用修改后的服务器列表。
这一切都是可以借助单例模式来完成。
单例的优缺点
单例模式在许多情况下效果很好,但是这种模式是存在一些缺陷的,由于单例模式具有全局访问权限:
- 全局变量在某处可能已经被误改,但是开发人员仍然认为它们没有发生改变,而该变量还应用在程序的其他位置被使用。
- 可能会对一个对象创建多个引用。由于单例只创建一个对象,因此这种情况下会对同一个对象创建多个引用。
- 所有依赖于全局变量的类都会由于一个类的改变而紧密耦合为全局数据,从而无意中影响另一个类。
Tip:
在许多的实际应用中,我们只需创建一个对象,如线程池,缓存,对话框,注册表设置等。如果我们为每个应用程序创建多个实例。这样就会导致资源的过度使用。单例模式就没有这担忧了。
单例是一种经过时间考验的成熟方法。能够在不带来太多缺陷的情况下提供全局访问点。
当然,该模式也有几个缺点。当使用全局变量或类的实例化非常耗费资源但最终却没有使用到它们的情况下,单例的影响可以忽略不计。