单例模式算是最常见的设计模式了,也是面试中的高频测试点。这一篇就来总结下如何在python中实现单例模式。
单例模式
所谓单例模式,就是针对某一个类,不管实例化多少次,实例出来的对象都是同一个。
之所以需要用到单例模式,有两个主要原因。其一是在程序开发中很多对象用于全局的记录,这些对象不管在程序的哪个地方被调用都应该是指向同一个,例如日志对象。其二是因为对象的创建和销毁都是耗费资源的,如果对象的创建次数减少,势必能提高程序的整体运行效率。
对象创建过程
下面先来看看python类对象的创建过程是啥样的。
平时新建一个类通常会带一个__init__()
方法,如下
class MyClass:
def __init__(self):
pass
这里的__init__()
方法的作用是对实例话的对象进行初始化,例如设定一些类变量的初始值。
但是其实在初始化之前还需要先把实例给创建出来啊,没错,这一步编译器已经替我们完成了,如果我们要手动去实现实例对象的创建可以用__new__()
方法,如下
class MyClass:
def __init__(self):
print('initialize the object')
def __new__(cls, *args, **kwargs):
print('create a new object')
return super().__new__(cls)
在所有类的基类object
类中,__new__()
方法接受一个类名做为参数,然后返回该类的一个空的实例对象。所以如果我们要自己去实现__new__()
方法,必须用super()
关键字去调用基类中的__new__()
方法,并把自带的cls
参数传递进去,从而获得一个本类的实例对象。
这样当类实例化的时候,首先将类通过cls
参数传递到__new__()
方法中,返回一个对象然后通过self参数传递到__init__()
方法中。来测试下
if __name__ == '__main__':
o = MyClass()
打印结果为
create a new object
initialize the object
值得注意的是__new__()
方法只要返回一个对象即可,不一定是本类的一个实例对象,例如
class MyClass:
def __init__(self):
print('initialize the object')
def __new__(cls, *args, **kwargs):
print('create a new object')
# return super().__new__(cls)
return TempClass()
class TempClass:
def __str__(self):
return 'This is a temp class'
这里在__new__()
方法里面返回的是一个TempClass
的对象,再来测试下
if __name__ == '__main__':
o = MyClass()
print(o)
打印的结果如下
create a new object
This is a temp class
可以看到并没有执行MyClass
类的__init__()
方法。
假如__new__()
方法没有返回任何的实例对象,则实例根本就不会创建成功,例如
class MyClass:
def __init__(self):
print('initialize the object')
def __new__(cls, *args, **kwargs):
print('create a new object')
if __name__ == '__main__':
o = MyClass()
最后只会打印
create a new object
代码实现
好了,了解了类对象的创建过程,就可以试着实现下单例模式了。
最简单的实现代码如下
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
print('init successfully')
定义了一个类变量_instance
,初始状态为None,当第一次实例化时,创建一个空实例对象,并将_instance
指向该实例变量。以后再次尝试实例化时,会直接将第一次创建的实例对象返回给__init__()
方法,从而达到每次都是同一个对象的目的。
下面来验证下
if __name__ == '__main__':
s1 = Singleton()
s2 = Singleton()
print(id(s1))
print(id(s2))
打印结果为
init successfully
init successfully
1410611251656
1410611251656
两个对象的内存地址相同,可见是同一个对象。
值得注意的是,虽然对象是同一个对象,但是在不同的地方可以有不同的初始化参数,例如
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, x, y):
print('init successfully')
self.x = x
self.y = y
if __name__ == '__main__':
s1 = Singleton(1, 2)
s2 = Singleton(3, 4)
print(id(s1))
print(id(s2))
print(s1.x, s1.y)
print(s2.x, s2.y) # 单例模式虽然实例id相同,但是init可以多遍
大家猜一猜最后打印的结果如何
init successfully
init successfully
2281646099528
2281646099528
3 4
3 4
可以看到第二次初始化将第一次初始化覆盖了,这就有点类似于全局变量,在两个地方被赋值一样。其实单例模式最初就是为了丰富全局变量的功能而出现的。
多线程
表面上看上面的代码已经达到目的了,但是其实还不够。
假设有10个线程同时要对类进行实例化,如下
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, x, y):
# print('init successfully')
self.x = x
self.y = y
def func():
obj = Singleton(1, 2)
print(id(obj))
if __name__ == '__main__':
for i in range(10):
thread = threading.Thread(target=func)
thread.start()
这样打印出来id也是一样的。
对于python多线程和多进程感兴趣的朋友可以看看另一篇博客《python多进程和多线程看这一篇就够了》
但是,假设为了模拟一个耗时的实例创建过程,人为修改一下__new__()
方法如下
def __new__(cls, *args, **kwargs):
if cls._instance is None:
time.sleep(0.05) # 注意这里添加了一行
cls._instance = super().__new__(cls)
return cls._instance
现在的打印结果如下
1818850265416
1818850265800
1818850265928
1818850266312
1818850266440
1818850266824
1818850266952
1818850267080
1818850266440
1818850266952
可见并没有实现单例模式,原因就在于0.05秒的等待时间内,10个线程在几乎都到达time.sleep(0.05)
这一句,然后等待结束各自创建自己的对象。而如果不等待的化,第一个线程创建对象完毕,第二个线程就按照单例模式的逻辑直接返回了。
这其实和多线程去操作全局变量的问题一样,都需要通过加锁去解决,也就是只有获得了锁的线程才能够去开始新对象的创建逻辑,如下
class Singleton:
_instance = None
lock = threading.Lock()
def __new__(cls, *args, **kwargs):
with cls.lock:
if cls._instance is None:
time.sleep(0.05)
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, x, y):
# print('init successfully')
self.x = x
self.y = y
通过with cls.lock
达到对整个逻辑加锁的目的,这之后打印的结果就是统一的了。
对于python多线程和锁感兴趣的朋友可以看看另一篇博客《python多进程和多线程看这一篇就够了》
这样子就已经挺好了,但是还不够完美。
锁的创建和释放都是消耗资源的,在上面的代码中每次创建都必须要获得锁,但是其实当对象已经创建了以后是可以直接返回不用加锁的,所以可以最后优化如下
class Singleton:
_instance = None
lock = threading.Lock()
def __new__(cls, *args, **kwargs):
if cls._instance:
return cls._instance
else:
with cls.lock:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, x, y):
# print('init successfully')
self.x = x
self.y = y
这样基本就是一个比较完整的单例模式实现的答案了,面试的时候这样写出来就不会有任何问题了。
import模块来实现单例模式
实际使用中会采用更为简洁的方式来实现单例模式,也就是import模块的方式。
例如有一个叫global_var.py
的文件如下
class Singleton:
pass
s = Singleton()
然后创建一个test_import.py
如下
from global_var import s
print(id(s))
以及test_import2.py
如下
from global_var import s
import test_import
print(id(s))
import关键字其实就是将对应的文件中的对象读入内存,假设在后续程序的运行中再次进行了import,并不会重新导入,而是直接使用内存中的内容,所以在test_import2.py
中首先导入s对象,然后执行import test_import
就是执行test_import.py
中的内容,这里再次导入s对象,就只会直接使用内存中的s对象,从而达到单例的目的,所以打印的id结果是一致的。
而且这种方式也是线程安全的。
总结
第二种import模块的方式在很多框架的源码中都是可以看到的,实际使用中很常见,在下一篇文章中我还会以logging日志对象为例来进行实际项目使用的举例。不过面试的时候还是考察第一种方式。
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。