导言
在软件设计中,有时确实存在一个类仅能用来产生一个唯一对象的必要性,例如,一个大公司的打印室虽然可以有多台打印机,但是其打印管理系统中只有一个打印任务控制对象,该对象管理打印排队并分配打印任务给各个打印机。再如,在 Windows 系统中,应该只有一个文件系统与一个文件视窗管理系统(Window Manager)。
怎样确保一个类只有一个实例,并且该实例可以容易的获得呢?有两个方法解决该问题,一个是程序员在应用程序中使用代码保证仅有一个实例被创建,另外一个方法是不依靠应用程序员,而是精心设计需要仅有一个实例的类,由该类本身的结构确保其仅能够被创建一个实例。很明显,第一个方法有许多缺点,因为程序员很可能疏忽而导致第二个实例被创建。实践证明,第二个方法是从根本上保证仅有一个实例被创建的有效方法。这就是单例模式(Singleton Pattern)所要表述的内容。
单例模式是指确保一个类仅有一个唯一的实例,并且提供一个全局的访问点。
Java 实现单例模式的思路是,为了防止客户程序利用构造方法创建多个对象,将构造方法声明为 private 类型。其原因是,如果构造方法是 public 类型的,则客户程序永远可以使用该类构造方法创建不同的对象。但这样做的问题是,如果一个类的构造方法是 private 的,则其他类就无法使用该类的构造方法来创建对象,从而该类就成为不可用的。
为了解决这个问题,该类必须提供一个可以获得实例的方法,通常称为 getInstance 方法。该方法返回一个类的实例。
我们可以发现要想实现单例模式,“私有”成了一个关键字。然而,在 Python 中,并没有绝对的私有,撑死只能用两个下划线开头实现伪私有。即使如此,Python 依旧可以实现单例模式,只不过有风险,具体有什么风险,后面再说。我们先实现一下单例模式,Python 实现单例模式最简单的方法是使用模块。把类和该类的一个实例对象单独放在一个模块,然后只需要导入该类的实例即可。刚刚我说有风险,现在大家应该明白为什么有风险了吧?如果我导入的不是实例变量,而是类本身,那不就违背单例模式了吗?这种方法虽然简单,但是有一定的风险,所以我建议换一种方法来实现单例模式。我们先想一下,Python 创建一个对象的过程是怎样的?刚学 Python 的人会认为就是执行 __init__,其实不是,在 __init__ 之前还要执行一个魔法方法 __new__!我们先看一下 __new__ 方法的定义。
@staticmethod # known case of __new__
def __new__(cls, *more): # known special case of object.__new__
""" Create and return a new object. See help(type) for accurate signature. """
pass
稍微翻译一下,我们就知道 __new__ 的作用:创建并返回一个新的对象。既然如此,我们就可以使用重写 __new__ 魔法方法来实现单例模式。
class President:
instance = None
def __new__(cls, *args, **kwargs):
if not isinstance(cls.instance, President):
cls.instance = object.__new__(cls)
return cls.instance
president1 = President()
president2 = President()
print(president1 is president2)
这段代码运行结果是 True,说明两个变量指向了同一块内存地址,那一块内存地址存放的就是这个类的实例。其实这么写还是有问题,如果我在外部修改静态属性 instance 就可以破坏单例模式的规则了,代码如下:
class President:
instance = None
def __new__(cls, *args, **kwargs):
if not isinstance(cls.instance, President):
cls.instance = object.__new__(cls)
return cls.instance
president1 = President()
President.instance = None
president2 = President()
print(president1 is president2)
这样的话运行结果就是 False 了,说明两个变量指向了不同的内存地址,也就是说类被实例化了两次。所以这种方法还是尽量避免吧。其实,在 Python 中,有一个神秘而有强大的神器,它可以动态的修改一个函数和一个类的功能,它就是装饰器!我们可以用它来装饰一个类,使其只能生成一个实例,代码如下:
def singleton(cls):
instance = {}
def get_instance(*args, **kwargs):
if cls not in instance:
instance[cls] = cls(*args, **kwargs)
return instance[cls]
return get_instance
@singleton
class President:
pass
if __name__ == '__main__':
president1 = President()
president2 = President()
print(president1 is president2)
运行结果为 True,这种方法看上去没有之前的风险了,也实现了单例模式,但是还有一个问题,在多线程的情况下,还能正确的运行吗?我们先尝试定义 10 个线程,每个线程实例化一个对象,代码如下:
from threading import Thread
def singleton(cls):
instance = {}
def get_instance(*args, **kwargs):
if cls not in instance:
instance[cls] = cls(*args, **kwargs)
return instance[cls]
return get_instance
@singleton
class President:
pass
def target():
president = President()
print(president)
if __name__ == '__main__':
threads = [Thread(target=target)for i in range(10)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
运行结果如图所示:
从结果中可以看出每个线程创建的对象指向同一个内存,发现没什么问题,但并不代表没有问题,为什么这么说?是因为Python有一把超级大锁 GIL(Global Interpreter Lock,全局解释器锁),这把锁保证一个时间只有一个线程,除非当前运行的线程遇到阻塞才会有线程切换,那么我们就阻塞一下看看,代码如下:
from threading import Thread
from time import sleep
def singleton(cls):
instance = {}
def get_instance(*args, **kwargs):
if cls not in instance:
instance[cls] = cls(*args, **kwargs)
return instance[cls]
return get_instance
@singleton
class President:
def __init__(self):
sleep(1)
def target():
president = President()
print(president)
if __name__ == '__main__':
threads = [Thread(target=target)for i in range(10)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
在这里我通过sleep来阻塞线程,目的是看在线程频繁切换的过程中还能不能正确的运行,运行结果如图所示:
从运行结果中可以看出,每个线程创建的对象指向了不同的内存,所以我们还需要给 get_instance 方法做一个同步,这里使用装饰器来做同步,代码如下:
from threading import Thread, Lock
from time import sleep
def synchronized(func):
lock = Lock()
def synchronized_func(*args, **kwargs):
with lock:
return func(*args, **kwargs)
return synchronized_func
def singleton(cls):
instance = {}
@synchronized
def get_instance(*args, **kwargs):
if cls not in instance:
instance[cls] = cls(*args, **kwargs)
return instance[cls]
return get_instance
@singleton
class President:
def __init__(self):
sleep(1)
def target():
president = President()
print(president)
if __name__ == '__main__':
threads = [Thread(target=target)for i in range(10)]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
运行结果如图所示:
不容易啊!费了这么多功夫,终于实现了一个目前看来没有任何问题的单例模式了!
本文分享自 Python机器学习算法说书人 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!