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 实现归并排序算法
Jun 05 Python
Python multiprocessing.Manager介绍和实例(进程间共享数据)
Nov 21 Python
Python中用于去除空格的三个函数的使用小结
Apr 07 Python
python实现计算倒数的方法
Jul 11 Python
一张图带我们入门Python基础教程
Feb 05 Python
Python内存管理方式和垃圾回收算法解析
Nov 11 Python
python 获取文件下所有文件或目录os.walk()的实例
Apr 23 Python
python正则表达式的懒惰匹配和贪婪匹配说明
Jul 13 Python
分享PyCharm最新激活码(真永久激活方法)不用每月找安装参数或最新激活码了
Dec 27 Python
Python实现王者荣耀自动刷金币的完整步骤
Jan 22 Python
手把手教你配置JupyterLab 环境的实现
Feb 02 Python
Django和Ueditor自定义存储上传文件的文件名
Feb 25 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个人网站架设连环讲(二)
2006/10/09 PHP
Smarty Foreach 使用说明
2010/03/23 PHP
PHP对象Object的概念 介绍
2012/06/14 PHP
JavaScript实现滚动栏效果的方法
2015/04/27 PHP
PHP中preg_match函数正则匹配的字符串长度问题
2015/05/27 PHP
PHP使用Mysqli类库实现完美分页效果的方法
2016/04/07 PHP
php使用GD2绘制几何图形示例
2017/02/15 PHP
Laravel 实现Eloquent模型分组查询并返回每个分组的数量 groupBy()
2019/10/23 PHP
无语,javascript居然支持中文(unicode)编程!
2007/04/12 Javascript
关于JS数组追加数组采用push.apply的问题
2014/06/09 Javascript
轻量级网页遮罩层jQuery插件用法实例
2015/07/31 Javascript
jQuery自定义动画函数实例详解(附demo源码)
2015/12/10 Javascript
JQuery fileupload插件实现文件上传功能
2016/03/18 Javascript
Centos7 中安装 Node.js v4.4.4
2016/11/03 Javascript
jQuery使用ajax方法解析返回的json数据功能示例
2017/01/10 Javascript
Bootstrap进度条与AJAX后端数据传递结合使用实例详解
2017/04/23 Javascript
JS库particles.js创建超炫背景粒子插件(附源码下载)
2017/09/13 Javascript
微信小程序使用map组件实现检索(定位位置)周边的POI功能示例
2019/01/23 Javascript
JavaScript实现身份证验证代码实例
2019/08/26 Javascript
微信小程序点击滚动到指定位置的实现
2020/05/22 Javascript
js实现车辆管理系统
2020/08/26 Javascript
js实现简易计算器小功能
2020/11/18 Javascript
Python中http请求方法库汇总
2016/01/06 Python
利用selenium 3.7和python3添加cookie模拟登陆的实现
2017/11/20 Python
python实现屏保计时器的示例代码
2018/08/08 Python
详解python中list的使用
2019/03/15 Python
python tkinter实现彩球碰撞屏保
2019/07/30 Python
基于python 等频分箱qcut问题的解决
2020/03/03 Python
浅谈Keras参数 input_shape、input_dim和input_length用法
2020/06/29 Python
Python Request类源码实现方法及原理解析
2020/08/17 Python
PUMA官方商城:世界领先的运动品牌之一
2016/11/16 全球购物
印尼综合在线预订网站:Tiket.com(机票、酒店、火车、租车和娱乐)
2018/10/11 全球购物
财务助理岗位职责
2013/11/10 职场文书
大学生最新职业生涯规划书范文
2014/01/12 职场文书
Mysql 如何批量插入数据
2021/04/06 MySQL
python数据分析之用sklearn预测糖尿病
2021/04/22 Python