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程序中使用SQLAlchemy时出现乱码的解决方案
Apr 24 Python
用实例解释Python中的继承和多态的概念
Apr 27 Python
python模块简介之有序字典(OrderedDict)
Dec 01 Python
Django使用中间键实现csrf认证详解
Jul 22 Python
Python创建一个元素都为0的列表实例
Nov 28 Python
PyCharm无法引用自身项目解决方式
Feb 12 Python
Python递归调用实现数字累加的代码
Feb 25 Python
python实现翻译word表格小程序
Feb 27 Python
Python基于pandas爬取网页表格数据
May 11 Python
python文件及目录操作代码汇总
Jul 08 Python
利用python3筛选excel中特定的行(行值满足某个条件/行值属于某个集合)
Sep 04 Python
Python+OpenCV检测灯光亮点的实现方法
Nov 02 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 Ajax实现页面无刷新发表评论
2007/01/02 PHP
在PHP中使用Sockets 从Usenet中获取文件
2008/01/10 PHP
fgetcvs在linux的问题
2012/01/15 PHP
在wamp集成环境下升级php版本(实现方法)
2013/07/01 PHP
php官方微信接口大全(微信支付、微信红包、微信摇一摇、微信小店)
2015/12/21 PHP
PHP基于Closure类创建匿名函数的方法详解
2017/08/17 PHP
PHP实现获取ip地址的5种方法,以及插入用户登录日志操作示例
2019/02/28 PHP
基于jquery的一个浮动框(扩展性比较好 )
2010/08/27 Javascript
深入理解JavaScript 闭包究竟是什么
2013/04/12 Javascript
javascript 寻找错误方法整理
2014/06/15 Javascript
c#+jquery实现获取radio和checkbox的值
2020/09/12 Javascript
javascript事件模型实例分析
2015/01/30 Javascript
在JavaScript应用中使用RequireJS来实现延迟加载
2015/07/01 Javascript
详解Bootstrap四种图片样式
2016/01/04 Javascript
详解jQuery UI库中文本输入自动补全功能的用法
2016/04/23 Javascript
Bootstrap基本样式学习笔记之标签(5)
2016/12/07 Javascript
基于jQuery实现弹幕APP
2017/02/10 Javascript
原生JS实现左右箭头选择日期实例代码
2017/03/14 Javascript
AngularJS实用基础知识_入门必备篇(推荐)
2017/07/10 Javascript
zTree树形菜单交互选项卡效果的实现方法
2017/12/25 Javascript
详解微信小程序实现跑马灯效果(附完整代码)
2019/04/29 Javascript
vue搜索和vue模糊搜索代码实例
2019/05/07 Javascript
Vue表单绑定的实例代码(单选按钮,选择框(单选时,多选时,用 v-for 渲染的动态选项)
2019/05/13 Javascript
如何利用javascript接收json信息并进行处理
2020/08/06 Javascript
[01:00:13]完美世界DOTA2联赛 LBZS vs Forest 第一场 11.07
2020/11/09 DOTA
Python实现在Linux系统下更改当前进程运行用户
2015/02/04 Python
Python元组操作实例分析【创建、赋值、更新、删除等】
2017/07/24 Python
Python矩阵常见运算操作实例总结
2017/09/29 Python
python leetcode 字符串相乘实例详解
2018/09/03 Python
python写程序统计词频的方法
2019/07/29 Python
二年级班级文化建设方案
2014/05/10 职场文书
2016年小学“我们的节日·中秋节”活动总结
2016/04/05 职场文书
实习报告范文之电话客服岗位
2019/07/26 职场文书
php中pcntl_fork详解
2021/04/01 PHP
鲲鹏 CentOS 7 安装Python3.7
2022/05/11 Servers
mysql5.5中文乱码问题解决的有用方法
2022/05/30 MySQL