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中DOM方法的动态性
Apr 11 Python
快速了解Python开发中的cookie及简单代码示例
Jan 17 Python
Python使用pydub库对mp3与wav格式进行互转的方法
Jan 10 Python
详解重置Django migration的常见方式
Feb 15 Python
Python选择网卡发包及接收数据包
Apr 04 Python
python脚本调用iftop 统计业务应用流量的思路详解
Oct 11 Python
将tensorflow.Variable中的某些元素取出组成一个新的矩阵示例
Jan 04 Python
浅谈python元素如何去重,去重后如何保持原来元素的顺序不变
Feb 28 Python
Pycharm pyuic5实现将ui文件转为py文件,让UI界面成功显示
Apr 08 Python
Python2及Python3如何实现兼容切换
Sep 01 Python
python 两种方法修改文件的创建时间、修改时间、访问时间
Sep 26 Python
Python Pandas读取Excel日期数据的异常处理方法
Feb 28 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编写一个简单的路由类
2011/04/13 PHP
php中eval函数的危害与正确禁用方法
2014/06/30 PHP
php中current、next与reset函数用法实例
2014/11/17 PHP
php基于GD库画五星红旗的方法
2015/02/24 PHP
js两行代码按指定格式输出日期时间
2011/10/21 Javascript
广泛收集的jQuery拖放插件集合
2012/04/09 Javascript
js实现日历可获得指定日期周数及星期几示例分享(js获取星期几)
2014/03/14 Javascript
Vue.js实战之组件之间的数据传递
2017/04/01 Javascript
webpack学习--webpack经典7分钟入门教程
2017/06/28 Javascript
js下拉菜单生成器dropMenu使用方法详解
2017/08/01 Javascript
JS获取当前地理位置的方法
2017/10/25 Javascript
Vue Socket.io源码解读
2018/02/07 Javascript
json数据传到前台并解析展示成列表的方法
2018/08/06 Javascript
react组件从搭建脚手架到在npm发布的步骤实现
2019/01/09 Javascript
vue cli使用融云实现聊天功能的实例代码
2019/04/19 Javascript
纯js实现无缝滚动功能代码实例
2020/02/21 Javascript
vue element和nuxt的使用技巧分享
2021/01/14 Vue.js
利用Python实现简单的相似图片搜索的教程
2015/04/23 Python
基于python中staticmethod和classmethod的区别(详解)
2017/10/24 Python
Python线程协作threading.Condition实现过程解析
2020/03/12 Python
Python实现汇率转换操作
2020/05/03 Python
Keras SGD 随机梯度下降优化器参数设置方式
2020/06/19 Python
捷克钓鱼用品网上商店:Parys.cz
2018/06/15 全球购物
Linux如何命名文件--使用文件名时应注意
2014/05/29 面试题
Shell脚本如何向终端输出信息
2014/04/25 面试题
毕业生就业自荐信
2013/12/04 职场文书
天网工程实施方案
2014/03/26 职场文书
如何写求职信
2014/05/24 职场文书
领导班子个人对照检查材料(群众路线)
2014/09/26 职场文书
大明湖导游词
2015/02/03 职场文书
2015年小学重阳节活动总结
2015/07/29 职场文书
python实现web邮箱扫描的示例(附源码)
2021/03/30 Python
Pytest中skip和skipif的具体使用方法
2021/06/30 Python
SQL优化老出错,那是你没弄明白MySQL解释计划用法
2021/11/27 MySQL
叶县这家生产军用电台的兵工厂,人称“四机部”,走出一上将
2022/02/18 无线电
Promise静态四兄弟实现示例详解
2022/07/07 Javascript