Python实现Singleton模式的方式详解


Posted in Python onAugust 08, 2019

前言

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

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

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

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

1.装饰器

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

def f(...):
  ...
f = staticmethod(f)
@staticmethod
def f(...):
  ...

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

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

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

2.闭包方式

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

def singleton(cls):
  instances = {}  
  def getinstance(*args, **kwargs):
    if cls not in instances:
      instances[cls] = cls(*args, **kwargs)
    return instances[cls]
  return getinstance
@singleton
class my_cls(object):
  pass

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

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

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

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

3.元类方式

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

class Singleton(type):
  def __init__(cls, name, bases, dic):
    super(Singleton, cls).__init__(name, bases, dic)
    cls._instance = None
  def __call__(cls, *args, **kwargs):
    if cls._instance is None:
      cls._instance = super(Singleton, cls).__call__(*args, **kwargs)
      # cls._instance = cls(*args, **kwargs)  # Error! Lead to call this function recursively
    return cls._instance

class my_cls(object):
  __metaclass__ = Singleton

这个例子中我们使用元类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__方式

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

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

class Singleton(object):
  _INSTANCE = {}
  def __init__(self, cls):
    self.cls = cls    
  def __call__(self, *args, **kwargs):
    instance = self._INSTANCE.get(self.cls, None)
    if not instance:
      instance = self.cls(*args, **kwargs)
      self._INSTANCE[self.cls] = instance
    return instance  
  def __getattr__(self, key):
    return getattr(self.cls, key, None)
@Singleton
class my_cls(object):
  pass

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

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

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

5.类本身实现方式

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

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

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

class MSC(object):
  _INSTANCE = None
  def __new__(cls, *args, **kwargs):
    if not cls._INSTANCE:
      cls._INSTANCE = super(MSC, cls).__new__(cls, *args, **kwargs)
      # cls._INSTANCE.args = args
      # cls._INSTANCE.kwargs = kwargs
    return cls._INSTANCE
  def __init__(self, *args, **kwargs):
    pass

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

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

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

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

6.替换__new__方式

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

def Singleton(cls):
  _instance = {}
  cls._origin_new = cls.__new__
  cls._origin_init = cls.__init__
  @functools.wraps(cls.__new__)
  def _singleton_new(cls, *args, **kwargs):
    if cls not in _instance:
      sin_instance = cls._origin_new(cls, *args, **kwargs)
      sin_instance._origin_init(*args, **kwargs)
      _instance[cls] = sin_instance
    return _instance[cls]
  # As a special case,__new__ is a staticmethod, need convert function to staticmethod by self 
  cls.__new__ = staticmethod(_singleton_new)
  # setattr(cls, '__new__', staticmethod(_singleton_new))
  cls.__init__ = lambda self, *args, **kwargs: None
  # setattr(cls, '__init__', lambda self, *args, **kwargs: None)
  return cls
@Singleton
class my_cls(object):
  pass

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

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

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

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

my_cls.__new__.func_closure[0].cell_contents 
== 
{<class '__main__.my_cls'>: <__main__.my_cls object at 0x02954810>}
==
_instance

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

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

7.注意事项

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

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

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

建议多关注语言特性的应用以及如何的解决实际的问题,不要沉迷于语言实现的一些细枝末节。本末倒置总会有些得不偿失嘛。尤其是python作为一种非常实用的语言。

本文介绍中如果有什么不当之处欢迎指正,如果有其他的更好的实现方式也请不吝赐教。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
用python写的一个wordpress的采集程序
Feb 27 Python
Python实现快速傅里叶变换的方法(FFT)
Jul 21 Python
Python基于OpenCV库Adaboost实现人脸识别功能详解
Aug 25 Python
Python实现的矩阵转置与矩阵相乘运算示例
Mar 26 Python
python基础梳理(一)(推荐)
Apr 06 Python
24式加速你的Python(小结)
Jun 13 Python
Django缓存系统实现过程解析
Aug 02 Python
Python.append()与Python.expand()用法详解
Dec 18 Python
Python基于pyjnius库实现访问java类
Jul 31 Python
Python unittest生成测试报告过程解析
Sep 08 Python
一篇文章教你用python画动态爱心表白
Nov 22 Python
Python中tkinter的用户登录管理的实现
Apr 22 Python
Python判断字符串是否xx开始或结尾的示例
Aug 08 #Python
详解解决Python memory error的问题(四种解决方案)
Aug 08 #Python
Python学习笔记之迭代器和生成器用法实例详解
Aug 08 #Python
pandas 选取行和列数据的方法详解
Aug 08 #Python
pandas 对日期类型数据的处理方法详解
Aug 08 #Python
解决Python设置函数调用超时,进程卡住的问题
Aug 08 #Python
Python 根据日志级别打印不同颜色的日志的方法示例
Aug 08 #Python
You might like
php class类的用法详细总结
2013/10/17 PHP
PHP编写RESTful接口
2016/02/23 PHP
修改file按钮的默认样式实现代码
2013/04/23 Javascript
jquery动态加载js/css文件方法(自写小函数)
2014/10/11 Javascript
jQuery浏览器CSS3特写兼容实例
2015/01/19 Javascript
基于JQuery实现仿网易邮箱全屏动感滚动插件fullPage
2015/09/20 Javascript
基于JavaScript代码实现随机漂浮图片广告
2016/01/05 Javascript
使用jQuery判断Div是否在可视区域的方法 判断div是否可见
2016/02/17 Javascript
jQuery ajax实现省市县三级联动
2021/03/07 Javascript
详解webpack进阶之loader篇
2017/08/23 Javascript
详解wow.js中各种特效对应的类名
2017/09/13 Javascript
Bootstrap-table自定义可编辑每页显示记录数
2018/09/07 Javascript
Node.js实现一个HTTP服务器的方法示例
2019/05/13 Javascript
JQuery样式与属性设置方法分析
2019/12/07 jQuery
Vertx基于EventBus发送接受自定义对象
2020/11/16 Javascript
python设置windows桌面壁纸的实现代码
2013/01/28 Python
Python中用于转换字母为小写的lower()方法使用简介
2015/05/19 Python
理解Python垃圾回收机制
2016/02/12 Python
Python 读取图片文件为矩阵和保存矩阵为图片的方法
2018/04/27 Python
python脚本实现验证码识别
2018/06/07 Python
python numpy 显示图像阵列的实例
2018/07/02 Python
python 接收处理外带的参数方法
2018/12/03 Python
python装饰器练习题及答案
2019/11/01 Python
python离线安装外部依赖包的实现
2020/02/13 Python
python利用百度云接口实现车牌识别的示例
2020/02/21 Python
使用Python发现隐藏的wifi
2020/03/04 Python
简单了解python shutil模块原理及使用方法
2020/04/28 Python
使用OpenCV去除面积较小的连通域
2020/07/05 Python
python学习之使用Matplotlib画实时的动态折线图的示例代码
2021/02/25 Python
幼儿园毕业园长感言
2014/02/24 职场文书
大学生优秀自荐信范文
2014/02/25 职场文书
校庆口号
2014/06/20 职场文书
大学生赌博检讨书
2014/09/22 职场文书
社区综治工作汇报
2014/10/27 职场文书
2015教师年度工作总结范文
2015/04/07 职场文书
管理失职检讨书范文
2015/05/05 职场文书