首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

Python实现Singleton模式的几种方式

使用python实现设计模式中的单例模式。单例模式是一种比较常用的设计模式,其实现和使用场景判定都是相对容易的。本文将简要介绍一下python中实现单例模式的几种常见方式和原理。一方面可以加深对python的理解,另一方面可以更加深入的了解该模式,以便实际工作中能更加灵活的使用单例设计模式。

本文将介绍常见的实现单例模式的几种方式,这里暂不考虑多线程的情况。

为了准备该篇博文,之前写了几篇相关的文章依次完整的介绍了相关的概念,下面会在需要的时候给出链接。

装饰器作为python实现单例模式的一种常用方法,先简单了解一下其概念。

1.装饰器

装饰器(Decorator)可以用作对函数以及类进行二次包裹或者封装,使用方式@wrapper。

上面这两种方式对函数的定义在语法上是等价的。当然对于类也有同样的用法,类可以作为装饰器也可以作为被装饰对象。唯一的区别就是经过包裹的类可能不在是一个类,而是一个类的对象或者一个函数,这取决于装饰器返回的值。

经过Decorator装饰的类或者函数本质上已经不再是原来的类或者函数了。但是,实际上在包裹之后得到的新对象仍然拥有被包裹对象的特性(这句是不是废话:-))。

在python中我们经常只需要实现一个装饰器,然后使用该装饰器作用于只能有唯一一个实例的类。这样只需要实现一个这样的装饰器,便可以作用于任何一个想要唯一实例的类。

2.闭包方式

闭包的应用很多,单例模式则是其应用之一。先看代码:

这个实现单例模式的方式将原来类的定义隐藏在闭包函数中,通过闭包函数及其中引用的自由变量来控制类对象的生成。由于唯一的实例存放在自由变量中,而且自由变量是无法直接在脚本层进行访问的。这种方式非常隐蔽的保护实例不被修改,因此很适合用于单例模式。

这种方式简单明了,很容易实现。但是如果不了解闭包实现过程和变量的绑定等概念可能会不明白其实现的过程。建议参考一下我的另一篇博文:理解python闭包概念。

这里一个很有趣的地方是为什么要使用instances = {}这样一个变量?可不可以不用字典,使用instance = None?如果singleton作为装饰器被多个不同的类使用,那么instance中会存在几个不同的实例么?

有时间可以思考一下这几个问题,答案也可以在我写的闭包相关的博文中找到。

3.元类方式

所谓单例模式,即我们需要控制类实例的生成过程,并且保证全局只可能存在一个唯一的实例。既然需要在创建类的对象过程中做些什么,应该很容易想到元类。

这个例子中我们使用元类Singleton替代默认使用type方式创建类my_cls。可以将类my_cls看做是元类Singleton的一个对象,当我们使用my_cls(...)的方式创建类my_cls的对象时,实际上是在调用元类Singleton的对象my_cls。

对象可以以函数的方式被调用,那么要求类中定义__call__函数。不过此处被调用的是类,因此我们在元类中定义函数__call__来控制类my_cls对象创建的唯一性。

这种方式的弊端之一就是类唯一的对象被存放在类的一个静态数据成员中,外部可以通过class_name._instance的方式修改甚至删除这个实例(该例中my_cls._instance = None完全合法)。

4.类作为装饰器之__call__方式

不仅函数可以作为装饰器,类也可以作为装饰器。

下面简单的介绍一下使用类作为装饰器实现单例模式的另一种方式。

函数作为装饰器返回的是一个函数,函数被调用过程中实际上是间接地调用其内部包裹的被装饰的对象。

类作为装饰器要想达到相同的效果只需要将类的对象返回,并且其对象是可以调用的。这是上面这个例子表达的一个核心思想。

这种方式写法很多,也很灵活,其思想基本上就是对被包裹对象的调用实际上调用的是类对象的__call__函数,该函数实际上是对被装饰对象的一次封装。

5.类本身实现方式

上面的例子中我们都是使用的装饰器或者元类的方式间接的通过控制类对象生成的方式来保证对象的唯一性,那么有没有办法直接在类中通过某种方式保证类对象的唯一性?

答案是肯定的。参考我之前写的一篇介绍元类的文章,可知生成对象前会调用函数__new__,如果__new__函数返回被创建的对象,那么会自动调用类中定义的__init__函数进行对象的初始化操作。

相信读了上面这句话,应该知道我们接下来要干什么了?没错,我们的目标就是__new__。

在这个例子中,我们完全可以理解为什么只会有一个类的对象会被创建。这种方式的定义决定了类本身只能被创建一个对象。

但是这里有一点需要注意,那就是不管创建多少MSC的对象,至始至终只会有一个对象,但是如果每次创建的时候传入的参数都不同,也就是__init__函数中参数不同,会导致同一个对象被多次初始化。

这种方式的弊端显然很明显,那就是该方法只能作用于单个类的定义。不能像上面的装饰器和元类,一次实现,可以到处使用。

那能不能将这个控制类生成过程的结构单独抽象出来呢?而且有没有什么方法能防止同一个对象多次被__init__初始化。下面我们看一种能被不同的类使用的更加抽象的结构。

6.替换__new__方式

我们定义的类作为一个对象,通过替换其部分属性可以达到控制类对象生成的目的。

上面我们通过替换类的__new__函数和__init__函数的方式,保证被Singleton装饰的类只有一个对象会被原来的__new__和__init__生成和初始化。

这里必须要替换类的__init__函数,而且该函数应该什么都不做。原因在于替换之后的__new__返回唯一的对象后,会自动调用现在的__init__函数。

原来的__init__函数已经在创建唯一一个对象时被调用过。而且只能被调用一次。

这里返回的并不是闭包结构,只是使用装饰器修改了类的部分属性,返回的仍是传入的类。但是类的__new__函数引用了Singleton中的local variable _instance。

Cell 对象my_cls.__new__.func_closure[0]中存放的便是类my_cls唯一的实例。

当然我们可以将my_cls唯一的对象作为类的一个静态数据成员放入cls.__dict__中来替代_instance = {},但是显然闭包结构更适合。

7.注意事项

文中借助python语言的类创建对象过程的相关原理,介绍了几种不同的单例模式实现方式。

为了保留被装饰对象的一些属性,可以使用@functools.wraps的方式对返回的闭包进行装饰。

平时建议使用前两种实现方式,也就是闭包方式和元类方式。其他情况多少有点玩弄python语法技巧的一些嫌疑,当然了,作为学习python来说还是比较有意义的。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20210109A0B9YP00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券