python 6种方法实现单例模式


Posted in Python onDecember 15, 2020

单例模式是一个软件的设计模式,为了保证一个类,无论调用多少次产生的实例对象,都是指向同一个内存地址,仅仅只有一个实例(只有一个对象)。

实现单例模式的手段有很多种,但总的原则是保证一个类只要实例化一个对象,下一次再实例的时候就直接返回这个对象,不再做实例化的操作。所以这里面的关键一点就是,如何判断这个类是否实例化过一个对象。

这里介绍两类方式:

  • 一类是通过模块导入的方式;
  • 一类是通过魔法方法判断的方式;
# 基本原理:
- 第一类通过模块导入的方式,借用了模块导入时的底层原理实现。
- 当一个模块(py文件)被导入时,首先会执行这个模块的代码,然后将这个模块的名称空间加载到内存。
- 当这个模块第二次再被导入时,不会再执行该文件,而是直接在内存中找。
- 于是,如果第一次导入模块,执行文件源代码时实例化了一个类,那再次导入的时候,就不会再实例化。

- 第二类主要是基于类和元类实现,在'对象'的魔法方法中判断是否已经实例化过一个对象
- 这类方式,根据实现的手法不同,又分为不同的方法,如:
- 通过类的绑定方法;通过元类;通过类下的__new__;通过装饰器(函数装饰器,类装饰器)实现等。

下面分别介绍这几种不同的实现方式,仅供参考实现思路,不做具体需求。

通过模块导入

# cls_singleton.py
class Foo(object):
  pass

instance = Foo()

# test.py
import cls_singleton

obj1 = cls_singleton.instance
obj2 = cls_singleton.instance
print(obj1 is obj2)

# 原理:模块第二次导入从内存找的机制

通过类的绑定方法

class Student:
  _instance = None	# 记录实例化对象

  def __init__(self, name, age):
    self.name = name
    self.age = age

  @classmethod
  def get_singleton(cls, *args, **kwargs):
    if not cls._instance:
      cls._instance = cls(*args, **kwargs)
    return cls._instance

stu1 = Student.get_singleton('jack', 18)
stu2 = Student.get_singleton('jack', 18)
print(stu1 is stu2)
print(stu1.__dict__, stu2.__dict__)

# 原理:类的绑定方法是第二种实例化对象的方式,
# 第一次实例化的对象保存成类的数据属性 _instance,
# 第二次再实例化时,在get_singleton中判断已经有了实例对象,直接返回类的数据属性 _instance

补充:这种方式实现的单例模式有一个明显的bug;bug的根源在于如果用户不通过绑定类的方法实例化对象,而是直接通过类名加括号实例化对象,那这样不再是单利模式了。

通过魔法方法__new__

class Student:

  _instance = None

  def __init__(self, name, age):
    self.name = name
    self.age = age

  def __new__(cls, *args, **kwargs):
    # if cls._instance:
    #   return cls._instance	        # 有实例则直接返回
    # else:
    #   cls._instance = super().__new__(cls)	# 没有实例则new一个并保存
    #   return cls._instance	        # 这个返回是给是给init,再实例化一次,也没有关系

    if not cls._instance:	            # 这是简化的写法,上面注释的写法更容易提现判断思路
      cls._instance = super().__new__(cls)
    return cls._instance


stu1 = Student('jack', 18)
stu2 = Student('jack', 18)
print(stu1 is stu2)
print(stu1.__dict__, stu2.__dict__)

# 原理:和方法2类似,将判断的实现方式,从类的绑定方法中转移到类的__new__中
# 归根结底都是 判断类有没有实例,有则直接返回,无则实例化并保存到_instance中。

补充:这种方式可以近乎完美地实现单例模式,但是依然不够完美。不完美的地方在于没有考虑到并发的极端情况下,有可能多个线程同一时刻实例化对象。关于这一点的补充内容在本文的最后一节介绍(!!!进阶必会)。

通过元类**

class Mymeta(type):

  def __init__(cls, name, bases, dic):
    super().__init__(name, bases, dic)
    cls._instance = None		         # 将记录类的实例对象的数据属性放在元类中自动定义了

  def __call__(cls, *args, **kwargs):	         # 此call会在类被调用(即实例化时触发)
    if cls._instance:				 # 判断类有没有实例化对象
      return cls._instance
    else:						 # 没有实例化对象时,控制类造空对象并初始化
      obj = cls.__new__(cls, *args, **kwargs)
      obj.__init__(*args, **kwargs)
      cls._instance = obj			     # 保存对象,下一次再实例化可以直接返回而不用再造对象
      return obj


class Student(metaclass=Mymeta):
  def __init__(self, name, age):
    self.name = name
    self.age = age


stu1 = Student('jack', 18)
stu2 = Student('jack', 18)
print(stu1 is stu2)
print(stu1.__dict__, stu2.__dict__)

# 原理:类定义时会调用元类下的__init__,类调用(实例化对象)时会触发元类下的__call__方法
# 类定义时,给类新增一个空的数据属性,
# 第一次实例化时,实例化之后就将这个对象赋值给类的数据属性;第二次再实例化时,直接返回类的这个数据属性
# 和方式3的不同之处1:类的这个数据属性是放在元类中自动定义的,而不是在类中显示的定义的。
# 和方式3的不同之处2:类调用时触发元类__call__方法判断是否有实例化对象,而不是在类的绑定方法中判断

函数装饰器

def singleton(cls):
  _instance_dict = {}		         # 采用字典,可以装饰多个类,控制多个类实现单例模式
 
  def inner(*args, **kwargs):
    if cls not in _instance_dict:
      _instance_dict[cls] = cls(*args, **kwargs)
    return _instance_dict.get(cls)
  return inner


@singleton
class Student:
  def __init__(self, name, age):
    self.name = name
    self.age = age

  # def __new__(cls, *args, **kwargs):	 # 将方法3的这部分代码搬到了函数装饰器中
  #   if not cls._instance:
  #     cls._instance = super().__new__(cls)
  #   return cls._instan
  
stu1 = Student('jack', 18)
stu2 = Student('jack', 18)
print(stu1 is stu2)
print(stu1.__dict__, stu2.__dict__)

类装饰器

class SingleTon:
  _instance_dict = {}

  def __init__(self, cls_name):
    self.cls_name = cls_name

  def __call__(self, *args, **kwargs):
    if self.cls_name not in SingleTon._instance_dict:
      SingleTon._instance_dict[self.cls_name] = self.cls_name(*args, **kwargs)
    return SingleTon._instance_dict.get(self.cls_name)


@SingleTon		               # 这个语法糖相当于Student = SingleTon(Student),即Student是SingleTon的实例对象
class Student:
  def __init__(self, name, age):
    self.name = name
    self.age = age

stu1 = Student('jack', 18)
stu2 = Student('jack', 18)
print(stu1 is stu2)
print(stu1.__dict__, stu2.__dict__)

# 原理:在函数装饰器的思路上,将装饰器封装成类。
# 程序执行到与语法糖时,会实例化一个Student对象,这个对象是SingleTon的对象。
# 后面使用的Student本质上使用的是SingleTon的对象。
# 所以使用Student('jack', 18)来实例化对象,其实是在调用SingleTon的对象,会触发其__call__的执行
# 所以就在__call__中,判断Student类有没有实例对象了。

!!!进阶必会

本部分主要是补充介绍多线程并发情况下,多线程高并发时,如果同时有多个线程同一时刻(极端条件下)事例化对象,那么就会出现多个对象,这就不再是单例模式了。

解决这个多线程并发带来的竞争问题,第一个想到的是加互斥锁,于是我们就用互斥锁的原理来解决这个问题。

解决的关键点,无非就是将具体示例化操作的部分加一把锁,这样同时来的多个线程就需要排队。

这样一来只有第一个抢到锁的线程实例化一个对象并保存在_instance中,同一时刻抢锁的其他线程再抢到锁后,不会进入这个判断if not cls._instance,直接把保存在_instance的对象返回了。这样就实现了多线程下的单例模式。

此时还有一个问题需要解决,后面所有再事例对象时都需要再次抢锁,这会大大降低执行效率。解决这个问题也很简单,直接在抢锁前,判断下是否有单例对象了,如果有就不再往下抢锁了(代码第11行判断存在的意义)。

import threading


class Student:

  _instance = None				# 保存单例对象
  _lock = threading.RLock()		    # 锁

  def __new__(cls, *args, **kwargs):
    
    if cls._instance:			# 如果已经有单例了就不再去抢锁,避免IO等待
      return cls._instance
    
    with cls._lock:				# 使用with语法,方便抢锁释放锁
      if not cls._instance:	
        cls._instance = super().__new__(cls, *args, **kwargs)
      return cls._instance

以上就是python 6种方法实现单例模式的详细内容,更多关于python 单例模式的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
跟老齐学Python之Python安装
Sep 12 Python
python 根据pid杀死相应进程的方法
Jan 16 Python
python使用多进程的实例详解
Sep 19 Python
python高级特性和高阶函数及使用详解
Oct 17 Python
OpenCV+face++实现实时人脸识别解锁功能
Aug 28 Python
wxPython实现带颜色的进度条
Nov 19 Python
python中从for循环延申到推导式的具体使用
Nov 29 Python
python怎么对数字进行过滤
Jul 05 Python
python小技巧——将变量保存在本地及读取
Nov 13 Python
python中str内置函数用法总结
Dec 27 Python
Python字符串对齐、删除字符串不需要的内容以及格式化打印字符
Jan 23 Python
Python中OpenCV实现查找轮廓的实例
Jun 08 Python
Ubuntu16安装Python3.9的实现步骤
Dec 15 #Python
Python爬虫开发与项目实战
Dec 16 #Python
python中reload重载实例用法
Dec 15 #Python
python 实现socket服务端并发的四种方式
Dec 14 #Python
linux centos 7.x 安装 python3.x 替换 python2.x的过程解析
Dec 14 #Python
Python获取指定网段正在使用的IP
Dec 14 #Python
python利用pytesseract 实现本地识别图片文字
Dec 14 #Python
You might like
详解PHP显示MySQL数据的三种方法
2008/06/05 PHP
新浪微博API开发简介之用户授权(PHP基础篇)
2011/09/25 PHP
PHP读取CSV大文件导入数据库的实例
2017/07/24 PHP
[转]JS宝典学习笔记
2007/02/07 Javascript
FileUpload上传图片(图片不变形)
2010/08/05 Javascript
javascript 兼容所有浏览器的DOM扩展功能
2012/08/01 Javascript
js禁止document element对象选中文本实现代码
2013/03/21 Javascript
JavaScript在IE和FF下的兼容性问题
2014/05/19 Javascript
JavaScript中的异常捕捉介绍
2014/12/31 Javascript
jQuery中next方法用法实例
2015/04/24 Javascript
跟我学习javascript的隐式强制转换
2015/11/16 Javascript
JavaScript中的return语句简单介绍
2015/12/07 Javascript
jQuery使用经验小技巧(推荐)
2016/05/31 Javascript
jQuery Tags Input Plugin(添加/删除标签插件)详解
2016/06/20 Javascript
headjs实现网站并行加载但顺序执行JS
2016/11/29 Javascript
ES6模块化的import和export用法方法总结
2017/08/08 Javascript
vue系列之动态路由详解【原创】
2017/09/10 Javascript
在vue项目中安装使用Mint-UI的方法
2017/12/27 Javascript
在vue中使用setInterval的方法示例
2019/04/16 Javascript
bootstrap 日期控件 datepicker被弹出框dialog覆盖的解决办法
2019/07/09 Javascript
移动端吸顶fixbar的解决方案详解
2019/07/17 Javascript
vue之debounce属性被移除及处理详解
2019/11/13 Javascript
Python多线程同步Lock、RLock、Semaphore、Event实例
2014/11/21 Python
Python的Flask框架中配置多个子域名的方法讲解
2016/06/07 Python
利用Anaconda完美解决Python 2与python 3的共存问题
2017/05/25 Python
Python切片操作实例分析
2018/03/16 Python
python cs架构实现简单文件传输
2020/03/20 Python
详解PyCharm配置Anaconda的艰难心路历程
2018/08/13 Python
python 画函数曲线示例
2019/12/04 Python
opencv 阈值分割的具体使用
2020/07/08 Python
英语专业学生个人求职信范文
2014/01/06 职场文书
2014年两会学习心得体会
2014/03/17 职场文书
2015年小学语文工作总结
2015/05/25 职场文书
风之谷观后感
2015/06/11 职场文书
医院病假条范文
2015/08/17 职场文书
公司董事任命书
2015/09/21 职场文书