单例模式是指让一个类只能创建出唯一的实例,这个题目在面试中出现的频率极高,因为它考察的不仅仅是单例模式,更是对Python语言到底掌握到何种程度,建议大家用装饰器和元类这两种方式来实现单例模式,因为这两种方式的通用性最强,而且也可以顺便展示自己对装饰器和元类中两个关键知识点的理解

比如,某个服务器程序的配置信息存放在一个文件中,客户端通过一个 AppConfig 的类来读取配置文件的信息。如果在程序运行期间,有很多地方都需要使用配置文件的内容,也就是说,很多地方都需要创建 AppConfig 对象的实例,这就导致系统中存在多个 AppConfig 的实例对象,而这样会严重浪费内存资源,尤其是在配置文件内容很多的情况下。事实上,类似 AppConfig 这样的类,我们希望在程序运行期间只存在一个实例对象。

  1. class Software(object):
  2. def __init__(self):
  3. pass
  4. if __name__ == '__main__':
  5. soft1 = Software()
  6. soft2 = Software()
  7. print(id(soft1)) # 2648813218168
  8. print(id(soft2)) # 2648813218560

上述代码创建了两个实例soft1、soft2,从输出地址上看,并不是同一个实例

方法一:使用new方法

参考:https://blog.csdn.net/qq_26442553/article/details/94393191

创建步骤:

  1. 定义一个类属性instance ,初始值是None,用于记录单例对象的引用
  2. 如果 类属性 is None,调用父类方法分配空间,并在类属性中记录结果

  3. 返回类属性中记录的对象引用 ```python class Singleton(object):

    1. 定义一个类属性,初始值是 None,用于记录单例对象的引用

    instance = None

    def new(cls, args, *kw):

    1. # 2. 判断类属性是否已经被赋值
    2. if cls.instance is None:
    3. # 3. 重写__new__方法
    4. cls.instance = super().__new__(cls)
    5. # 4. 返回类属性的单例引用
    6. return cls.instance

    def init(self):

    1. print('初始化对象')

class Software(Singleton): pass

if name == ‘main‘: soft1 = Software() print(soft1) soft2 = Software() print(soft2)

  1. 输出结果:
  2. ```python
  3. 初始化对象
  4. <__main__.Software object at 0x000001FCF81F31D0>
  5. 初始化对象
  6. <__main__.Software object at 0x000001FCF81F31D0>
  1. new
    1. 作用:
      1. 在内存中为对象分配空间
      2. 返回对象的引用
    2. new 是在一个对象实例化的时候所调用的第一个方法
    3. new 的第一个参数是这个类,其他参数是用来直接传递给 init 方法
    4. new 返回一个构建的实例
    5. new 只有返回一个cls实例时,后面的 init 才会被调用;
  2. 重写new方法是固定的 super().__new__(cls)

    Python 3 可以使用直接使用 super().xxx 代替 super(Class, self).xxx

  3. 凡是继承Singleton基类的子类都属于单例模式。可以从上面输出看得出来,我们虽然对Software类实例化两次,分别得到两个名为soft1和soft2的实例,对象在内存中地址是一样的,也就说这两个实例指向同一个地址,为同一个实例。


改进:只执行一次初始化操作

  1. 在每次使用 类名() 创建对象时,Python 的解释器都会自动调用两个方法:
    1. new 分配空间
    2. init 对象初始化
  2. 在上一小节对 new 方法改造之后,每次都会得到 第一次被创建对象的引用。但是:初始化方法还会被再次调用,我们改进代码,想让初始化只执行一次
  3. 解决步骤(在上述步骤的基础上)
    1. 定义一个类属性init_flag 标记是否 执行过初始化动作,初始值为 False
    2. init 方法中,判断 init_flag,如果为 False 就执行初始化动作
    3. 然后将 init_flag 设置为 True
    4. 这样,再次 自动 调用 init 方法时,初始化动作就不会被再次执行 了

改进代码

  1. class Singleton(object):
  2. # 1. 定义一个类属性,初始值是 None,用于记录单例对象的引用
  3. instance = None
  4. # 1.1. 定义一个类属性init_flag 标记是否 执行过初始化动作,初始值为 False
  5. init_flag = False
  6. def __new__(cls, *args, **kw):
  7. # 2. 判断类属性是否已经被赋值
  8. if cls.instance is None:
  9. # 3. 重写__new__方法
  10. cls.instance = super().__new__(cls)
  11. # 4. 返回类属性的单例引用
  12. return cls.instance
  13. def __init__(self):
  14. # 5.判断 init_flag,如果为 False 就执行初始化动作
  15. if not Singleton.init_flag:
  16. print('初始化对象')
  17. Singleton.init_flag = True
  18. class Software(Singleton):
  19. pass
  20. if __name__ == '__main__':
  21. soft1 = Software()
  22. print(soft1)
  23. soft2 = Software()
  24. print(soft2)

结果

  1. 初始化对象
  2. <__main__.Software object at 0x000001CE996C30B8>
  3. <__main__.Software object at 0x000001CE996C30B8>

可以看到只调用一次init 方法

方法二:装饰器

实现步骤:

  1. 创建一个装饰器函数singleton
  2. 创建一个字典用来保存被装饰类的实例对象
  3. 判断这个类有没有创建过对象

    1. 没有则新创建一个,保存在字典中
    2. 有则返回之前创建的 ```python def singleton(cls, args, *kw): “””单例类装饰器””” instances = {}

      def wrapper(): if cls not in instances:

      1. 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

  1. <a name="OjBPI"></a>
  2. # 方法三:使用metaclass元类
  3. ```python
  4. class Singleton(type):
  5. __instances = {}
  6. def __call__(cls, *args, **kwargs):
  7. if cls not in cls.__instances:
  8. cls.__instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
  9. return cls.__instances[cls]
  10. # python3写法
  11. class Software(metaclass=Singleton):
  12. def __init__(self):
  13. pass
  14. if __name__ == '__main__':
  15. soft1 = Software()
  16. soft2 = Software()
  17. print(id(soft1)) # 2193971814976
  18. 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方法,试想一下我们该怎么实现?
    • 有两种方法
      • 反复实例化、反复认证
      • 把实例化后的对象作为参数传入到每个用到select的函数里

        第一种:反复实例化、反复认证

        ```python host = “10.293.291.19” user = “admin” passwd = “666666” def use_data_1(): sql_client = SqlClient(host, user, passwd) sql_client.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()

单例模式缺点

不试用以下场景

  • 多线程
  • 可变对象

在这些场景下,它违背了单例模式单一性原则,而且很容易引起数据错误。
因此,使用单例模式之前需要考虑一下对应场景是否适合,如果适合,单例模式能够大大提高代码的效率,同时使得代码更加简洁,但是如果不适合而强行使用单例模式,那样会导致很多未知的问题。