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实现从脚本里运行scrapy的方法
Apr 07 Python
Perl中著名的Schwartzian转换问题解决实现
Jun 02 Python
Python基于回溯法子集树模板解决最佳作业调度问题示例
Sep 08 Python
浅谈Python使用Bottle来提供一个简单的web服务
Dec 27 Python
python找出一个列表中相同元素的多个索引实例
Jun 11 Python
opencv python在视屏上截图功能的实现
Mar 05 Python
python中关于数据类型的学习笔记
Jul 19 Python
PyTorch 导数应用的使用教程
Aug 31 Python
python中的插入排序的简单用法
Jan 19 Python
python解包用法详解
Feb 17 Python
python 获取计算机的网卡信息
Feb 18 Python
Python机器学习之KNN近邻算法
May 14 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
信用卡效验程序
2006/10/09 PHP
php+mysql写的简单留言本实例代码
2008/07/25 PHP
Ajax实时验证用户名/邮箱等是否已经存在的代码打包
2011/12/01 PHP
PHP连接SQLServer2005的实现方法(附ntwdblib.dll下载)
2012/07/02 PHP
PHP的魔术常量__METHOD__简介
2014/07/08 PHP
PHP实现批量生成App各种尺寸Logo
2015/03/19 PHP
PHP环境搭建(php+Apache+mysql)
2016/11/14 PHP
php实现数组中出现次数超过一半的数字的统计方法
2018/10/14 PHP
PHP静态方法和静态属性及常量属性的区别与介绍
2019/03/22 PHP
De facto standard 世界上不可思议的事实标准
2010/08/29 Javascript
浅谈javascript中createElement事件
2014/12/05 Javascript
JavaScript实现获取dom中class的方法
2015/02/09 Javascript
基于JQuery实现仿网易邮箱全屏动感滚动插件fullPage
2015/09/20 Javascript
详解支持Angular 2的表格控件
2017/01/19 Javascript
JavaScript正则表达式和级联效果
2017/09/14 Javascript
JS与CSS3实现图片响应鼠标移动放大效果示例
2018/05/04 Javascript
vue-router判断页面未登录自动跳转到登录页的方法示例
2018/11/04 Javascript
关于React动态加载路由处理的相关问题
2019/01/07 Javascript
vue组件间通信六种方式(总结篇)
2019/05/15 Javascript
python机器学习实战之最近邻kNN分类器
2017/12/20 Python
实用自动化运维Python脚本分享
2018/06/04 Python
python3在同一行内输入n个数并用列表保存的例子
2019/07/20 Python
python构建指数平滑预测模型示例
2019/11/21 Python
HTML5视频播放插件 video.js介绍
2018/09/29 HTML / CSS
澳大利亚女性快速时尚零售商:Ally Fashion
2018/04/25 全球购物
Giglio俄罗斯奢侈品购物网:男士、女士、儿童高级时装
2018/07/27 全球购物
迪卡侬比利时官网:Decathlon比利时
2019/12/28 全球购物
外贸采购员求职的自我评价
2013/11/26 职场文书
财会自我鉴定范文
2013/12/27 职场文书
国贸专业的职业规划范文
2014/01/23 职场文书
学校大课间活动方案
2014/01/30 职场文书
个人工作作风整改措施思想汇报
2014/10/13 职场文书
考研英语辞职信
2015/05/13 职场文书
大学生党课感想
2015/08/11 职场文书
2016年教师节特级教师获奖感言
2015/12/09 职场文书
关于python pygame游戏进行声音添加的技巧
2021/10/24 Python