• 设计模式之-单例设计模式
  • 发布于 2个月前
  • 141 热度
    0 评论
00.写在之前

学一门编程语言是一件很简单的事,学“会”一门编程语言却是很难的事,仅多了一个字,难度却是指数级的差距。前者显然只是学会语法,能写简单的程序,而后者却是要求熟练应用,得心应手的解决各种问题,这也是区分好的程序员和一般程序员的标准。

掌握解决问题的方法,能用自己擅长的语言解决出现的各种问题,这是在成为一个优秀程序员路上所要追求的目标。正因为如此,我们才需要去学习「设计模式」。

那么什么是设计模式呢?

设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长一段时间的试验和错误总结出来的。通俗点来说,就是对问题先进行分类,然后再确定一个比较好的解决方案。针对不同的问题用上不同的套路,这个就叫「设计模式」。

那么为什么要使用设计模式呢?

使用设计模式是为了可重用代码,让代码更容易被他人理解,保证代码的可靠性。总结一下就是为了“方便”。想想在工作中,我们一旦碰到了某类问题,就能用相应的套路去解决问题,这样我们在开发的时候就不用使劲拽头发的去思考这个问题怎么解决。

01.单例设计模式

「单例设计模式」估计对很多人来说都是一个陌生的概念,其实它就环绕在你的身边。比如我们每天必用的听歌软件,同一时间只能播放一首歌曲,所以对于一个听歌的软件来说,负责音乐播放的对象只有一个;再比如打印机也是同样的道理,同一时间打印机也只能打印一份文件,同理负责打印的对象也只有一个。

结合说的听歌软件和打印机都只有唯一的一个对象,就很好理解「单例设计模式」。

单例设计模式确保一个类只有一个实例,并提供一个全局访问点。

「单例」就是单个实例,我们在定义完一个类的之后,一般使用「类名()」的方式创建一个对象,而单例设计模式解决的问题就是无论执行多少遍「类名()」,返回的对象内存地址永远是相同的。

02.__new__ 方法

当我们使用「类名()」创建对象的时候,Python 解释器会帮我们做两件事情:第一件是为对象在内存分配空间,第二件是为对象进行初始化。初始化(__init__)我们已经学过了,那「分配空间」是哪一个方法呢?就是我们这一小节要介绍的 __new__ 方法。

那这个 __new__ 方法和单例设计模式有什么关系呢?单例设计模式返回的对象内存地址永远是相同的,这就意味着在内存中这个类的对象只能是唯一的一份,为达到这个效果,我们就要了解一下为对象分配空间的 __new__ 方法。

明确了这个目的以后,接下来让我们看一下 __new__ 方法。__new__ 方法在内部其实做了两件时期:第一件事是为「对象分配空间」,第二件事是「把对象的引用返回给 Python 解释器」。当 Python 的解释器拿到了对象的引用之后,就会把对象的引用传递给 __init__ 的第一个参数 self,__init__ 拿到对象的引用之后,就可以在方法的内部,针对对象来定义实例属性。

这就是 __new__ 方法和 __init__ 方法的分工。

总结一下就是:之所以要学习 __new__ 方法,就是因为需要对分配空间的方法进行改造,改造的目的就是为了当使用「类名()」创建对象的时候,无论执行多少次,在内存中永远只会创造出一个对象的实例,这样就可以达到单例设计模式的目的。

03.重写 __new__ 方法

在这里我用一个 __new__ 方法的重写来做一个演练:首先定义一个打印机的类,然后在类里重写一下 __new__ 方法。通过对这个方法的重写来强化一下 __new__ 方法要做的两件事情:在内存中分配内存空间 & 返回对象的引用。同时验证一下,当我们使用「类名()」创建对象的时候,Python 解释器会自动帮我们调用 __new__ 方法。

首先我们先定义一个打印机类 Printer,并创建一个实例:
class Printer():
    def __init__(self):
        print("打印机初始化")
# 创建打印机对象
printer = Printer()

接下来就是重写 __new__ 方法,在此之前,先说一下注意事项,只要⚠️了这几点,重写 __new__ 就没什么难度:

重写 __new__ 方法一定要返回对象的引用,否则 Python 的解释器得不到分配了空间的对象引用,就不会调用对象的初始化方法;
__new__ 是一个静态方法,在调用时需要主动传递 cls 参数。
# 重写 __new__ 方法
class Printer():
    def __new__(cls, *args, **kwargs):
        # 可以接收三个参数
        # 三个参数从左到右依次是 class,多值元组参数,多值的字典参数
        print("this is rewrite new")
        instance = super().__new__(cls)
        return instance
    def __init__(self):
        print("打印机初始化")
# 创建打印机对象
player = Printer()
print(player)

上述代码对 __new__ 方法进行了重写,我们先来看一下输出结果:

this is rewrite new
打印机初始化
<__main__.Printer object at 0x10fcd2ba8>

上述的结果打印出了 __new__ 方法和 __init__ 方法里的内容,同时还打印了类的内存地址,顺序正好是我们在之前说过的。__new__ 方法里的三行代码正好做了在本小节开头所说的三件事:

print(this is rewrite new):证明了创建对象时,__new__ 方法会被自动调用;
instance = super().__new__(cls):为对象分配内存空间(因为 __new__ 本身就有为对象分配内存空间的能力,所以在这直接调用父类的方法即可);
return instance:返回对象的引用。
04.设计单例模式

说了这么多,接下来就让我们用单例模式来设计一个单例类。乍一看单例类看起来比一般的类更唬人,但其实就是差别在一点:单例类在创建对象的时候,无论我们调用多少次创建对象的方法,得到的结果都是内存中唯一的对象。

可能到这有人会有疑惑:怎么知道用这个类创建出来的对象是同一个对象呢?其实非常的简单,我们只需要多调用几次创建对象的方法,然后输出一下方法的返回结果,如果内存地址是相同的,说明多次调用方法返回的结果,本质上还是同一个对象。
class Printer():
    pass

printer1 = Printer()
print(printer1)
printer2 = Printer()
print(printer2)

上面是一个一般类的多次调用,打印的结果如下所示:
<__main__.Printer object at 0x10a940780>
<__main__.Printer object at 0x10a94d3c8>

可以看出,一般类中多次调用的内存地址不同(即 printer1 和 printer2 是两个完全不同的对象),而单例设计模式设计的单例类 Printer(),要求是无论调用多少次创建对象的方法,控制台打印出来的内存地址都是相同的。

那么我们该怎么实现呢?其实很简单,就是多加一个「类属性」,用这个类属性来记录「单例对象的引用」。

为什么要这样呢?其实我们一步一步的来想,当我们写完一个类,运行程序的时候,内存中其实是没有这个类创建的对象的,我们必须调用创建对象的方法,内存中才会有第一个对象。在重写 __new__ 方法的时候,我们用 instance = super().__new__(cls) ,为对象分配内存空间,同时用 istance 类属性记录父类方法的返回结果,这就是第一个「对象在内存中的返回地址」。当我们再次调用创建对象的方法时,因为第一个对象已经存在了,我们直接把第一个对象的引用做一个返回,而不用再调用 super().__new__(cls) 分配空间这个方法,所以就不会在内存中为这个类的其它对象分配额外的内存空间,而只是把之前记录的第一个对象的引用做一个返回,这样就能做到无论调用多少次创建对象的方法,我们永远得到的是创建的第一个对象的引用。

这个就是使用单例设计模式解决在内存中只创建唯一一个实例的解决办法。下面我就根据上面所说的,来完成单例设计模式。
class Printer():
    instance = None
    def __new__(cls, *args, **kwargs):
        if cls.instance is None:
            cls.instance = super().__new__(cls)
    return cls.instance
printer1 = Printer()
print(printer1)
printer2 = Printer()
print(printer2)

上述代码很简短,首先给类属性复制为 None,在 __new__ 方法内部,如果 instance 为 None,证明第一个对象还没有创建,那么就为第一个对象分配内存空间,如果 instance 不为 None,直接把类属性中保存的第一个对象的引用直接返回,这样在外界无论调用多少次创建对象的方法,得到的对象的内存地址都是相同的。

下面我们运行一下程序,来看一下结果是不是能印证我们的说法:
<__main__.Printer object at 0x10f3223c8>
<__main__.Printer object at 0x10f3223c8>

上述输出的两个结果可以看出地址完全一样,这说明 printer1 和 printer2 本质上是相同的一个对象。
用户评论