单例模式是指让一个类只能创建出唯一的实例,这个题目在面试中出现的频率极高,因为它考察的不仅仅是单例模式,更是对Python语言到底掌握到何种程度,建议大家用装饰器和元类这两种方式来实现单例模式,因为这两种方式的通用性最强,而且也可以顺便展示自己对装饰器和元类中两个关键知识点的理解
比如,某个服务器程序的配置信息存放在一个文件中,客户端通过一个 AppConfig 的类来读取配置文件的信息。如果在程序运行期间,有很多地方都需要使用配置文件的内容,也就是说,很多地方都需要创建 AppConfig 对象的实例,这就导致系统中存在多个 AppConfig 的实例对象,而这样会严重浪费内存资源,尤其是在配置文件内容很多的情况下。事实上,类似 AppConfig 这样的类,我们希望在程序运行期间只存在一个实例对象。
class Software(object):
def __init__(self):
pass
if __name__ == '__main__':
soft1 = Software()
soft2 = Software()
print(id(soft1)) # 2648813218168
print(id(soft2)) # 2648813218560
上述代码创建了两个实例soft1、soft2,从输出地址上看,并不是同一个实例
方法一:使用new方法
参考:https://blog.csdn.net/qq_26442553/article/details/94393191
创建步骤:
- 定义一个类属性instance ,初始值是None,用于记录单例对象的引用
- 如果 类属性 is None,调用父类方法分配空间,并在类属性中记录结果
返回类属性中记录的对象引用 ```python class Singleton(object):
1. 定义一个类属性,初始值是 None,用于记录单例对象的引用
instance = None
def new(cls, args, *kw):
# 2. 判断类属性是否已经被赋值
if cls.instance is None:
# 3. 重写__new__方法
cls.instance = super().__new__(cls)
# 4. 返回类属性的单例引用
return cls.instance
def init(self):
print('初始化对象')
class Software(Singleton): pass
if name == ‘main‘: soft1 = Software() print(soft1) soft2 = Software() print(soft2)
输出结果:
```python
初始化对象
<__main__.Software object at 0x000001FCF81F31D0>
初始化对象
<__main__.Software object at 0x000001FCF81F31D0>
- new:
- 作用:
- 在内存中为对象分配空间
- 返回对象的引用
- new 是在一个对象实例化的时候所调用的第一个方法
- new 的第一个参数是这个类,其他参数是用来直接传递给 init 方法
- new 返回一个构建的实例
- new 只有返回一个cls实例时,后面的 init 才会被调用;
- 作用:
重写new方法是固定的
super().__new__(cls)
Python 3 可以使用直接使用 super().xxx 代替 super(Class, self).xxx
凡是继承Singleton基类的子类都属于单例模式。可以从上面输出看得出来,我们虽然对Software类实例化两次,分别得到两个名为soft1和soft2的实例,对象在内存中地址是一样的,也就说这两个实例指向同一个地址,为同一个实例。
改进:只执行一次初始化操作
- 在每次使用 类名() 创建对象时,Python 的解释器都会自动调用两个方法:
- new 分配空间
- init 对象初始化
- 在上一小节对 new 方法改造之后,每次都会得到 第一次被创建对象的引用。但是:初始化方法还会被再次调用,我们改进代码,想让初始化只执行一次
- 解决步骤(在上述步骤的基础上)
- 定义一个类属性init_flag 标记是否 执行过初始化动作,初始值为 False
- 在 init 方法中,判断 init_flag,如果为 False 就执行初始化动作
- 然后将 init_flag 设置为 True
- 这样,再次 自动 调用 init 方法时,初始化动作就不会被再次执行 了
改进代码
class Singleton(object):
# 1. 定义一个类属性,初始值是 None,用于记录单例对象的引用
instance = None
# 1.1. 定义一个类属性init_flag 标记是否 执行过初始化动作,初始值为 False
init_flag = False
def __new__(cls, *args, **kw):
# 2. 判断类属性是否已经被赋值
if cls.instance is None:
# 3. 重写__new__方法
cls.instance = super().__new__(cls)
# 4. 返回类属性的单例引用
return cls.instance
def __init__(self):
# 5.判断 init_flag,如果为 False 就执行初始化动作
if not Singleton.init_flag:
print('初始化对象')
Singleton.init_flag = True
class Software(Singleton):
pass
if __name__ == '__main__':
soft1 = Software()
print(soft1)
soft2 = Software()
print(soft2)
结果
初始化对象
<__main__.Software object at 0x000001CE996C30B8>
<__main__.Software object at 0x000001CE996C30B8>
方法二:装饰器
实现步骤:
- 创建一个装饰器函数singleton
- 创建一个字典用来保存被装饰类的实例对象
判断这个类有没有创建过对象
- 没有则新创建一个,保存在字典中
有则返回之前创建的 ```python def singleton(cls, args, *kw): “””单例类装饰器””” instances = {}
def wrapper(): if cls not in instances:
instances[cls] = cls(*args, **kw)
return instances[cls]
return wrapper
@singleton class Software(object): def init(self): pass
if name == ‘main‘: soft1 = Software() soft2 = Software() print(id(soft1)) # 2148233191720 print(id(soft2)) # 2148233191720
<a name="OjBPI"></a>
# 方法三:使用metaclass元类
```python
class Singleton(type):
__instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls.__instances:
cls.__instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls.__instances[cls]
# python3写法
class Software(metaclass=Singleton):
def __init__(self):
pass
if __name__ == '__main__':
soft1 = Software()
soft2 = Software()
print(id(soft1)) # 2193971814976
print(id(soft2)) # 2193971814976
- call()的作用是使实例能够像函数一样被调用、
单例模式实际运用
场景描述
做项目开发过程中,大多数岗位都会和数据打交道,无论是前端还是后端。假如,我们存储数据工具是SQL Server,我们需要通过host、user、passwd来连接数据库进行读取数据,这时候就需要一次认证,多次调用,请注意这句话,很关键。
实现一个连接SQL的类
class SqlClient(object):
def __init__(self, host, user, passwd):
self.host = host
self.user = user
self.passwd = passwd
self.register()
def register(self):
self.info = "{}--{}---{}".format(self.host, self.user, self.passwd)
def select(self):
print("SELECT * FROM {}".format(self.host))
- SqlClient中有3个方法,init用于初始化参数,register是认证SQL客户端,select是执行SQL语句的操作。
- 到这里,我们完成了SQL的认证,后面我们会在不同的地方查找数据,也就是在多个地方需要调用SqlClient类的select方法,试想一下我们该怎么实现?
def use_data_2(): sql_client = SqlClient(host, user, passwd) sql_client.select()
def use_data_3(): sql_client = SqlClient(host, user, passwd) sql_client.select()
use_data_1() use_data_2() use_data_3()
可以看到,我们在**use_data_1、use_data_2、use_data_3**三处使用到了SQL选择工具,每一次我们都要重新实例化SqlClient,显然,这是很麻烦的。
<a name="P3O67"></a>
## 第二种:把实例化后的对象作为参数传入到每个用到select的函数里
```python
host = "10.293.291.19"
user = "admin"
passwd = "666666"
def use_data_1(sql_client):
sql_client.select()
use_data_2(sql_client)
def use_data_2(sql_client):
use_data_3(sql_client)
def use_data_3(sql_client):
sql_client.select()
sql_client = SqlClient(host, user, passwd)
use_data_1(sql_client)
可以看到上述示例,use_data_1调用use_data_2,use_data_2调用use_data_3,而我们在use_data_1、use_data_3中需要用到SQL工具,但是在use_data_2这个中间环节用不到,但是为了让参数继续传递下去,sql_client却不得不作为use_data_2的一个入参。
单例模式
只需要实例化一次用于认证,然后再每个位置调用即可
class Singleton(object):
def __new__(cls, *args, **kw):
if not hasattr(cls, '_instance'):
orig = super(Singleton, cls)
cls._instance = orig.__new__(cls)
return cls._instance
class SqlClient(Singleton):
info = None
def register(self, host, user, passwd):
self.info = "{}--{}--{}".format(host, user, passwd)
def select(self):
print(self.info)
通过继承Singleton实现SqlClient的单例模式,我们只需要调用register一次,用于认证客户端,然后后期每次重新实例化都是指向的同一个实例,也就是已经认证过的示例,我们后面任何其他地方调用的地方直接使用select方法即可
def use_data_1():
SqlClient().select()
def use_data_2():
SqlClient().select()
def use_data_3():
SqlClient().select()
SqlClient().register(host, user, passwd)
use_data_1()
use_data_2()
use_data_3()
单例模式缺点
不试用以下场景
- 多线程
- 可变对象
在这些场景下,它违背了单例模式单一性原则,而且很容易引起数据错误。
因此,使用单例模式之前需要考虑一下对应场景是否适合,如果适合,单例模式能够大大提高代码的效率,同时使得代码更加简洁,但是如果不适合而强行使用单例模式,那样会导致很多未知的问题。