深入剖析Python的单例模式实现(一)

发表于:2023-8-30 09:56

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:忆想不到的晖    来源:稀土掘金

  一、引言
  单例模式是一种常见的设计模式,它限制一个类只能生成一个实例。在Python开发中,我们该如何实现单例模式呢?本文将通过一个简单的例子,使用Python的元类来实现一个线程安全的单例类,并比较说明使用装饰器实现单例的优劣。
  单例模式看起来简单,但是想要做到完全线程安全并支持子类继承,还有一定的难度。本文将从单例模式的概念和应用场景开始,一步步分析线程安全的单例类该如何设计,加锁来保证线程安全等。
  二、单例的应用场景
  1. 系统只需要一个实例对象,比如配置类、日志、工具类等。使用单例可以直接保证全局只存在一个实例。
  2. 控制资源访问,比如一个硬件资源只允许一个进程访问,或者打印机只允许一个任务执行打印操作。
  3. 频繁创建和销毁实例会带来较高的系统开销,使用单例可以减少内存占用和性能消耗。比如任务池、连接池等。
  4. 想确保一个类只有一个可见的实例,并提供一个全局访问点,如线程池、缓存、会话对象等。
  5. 当类状态需要频繁保存和恢复时,可以让类成为单例,避免每次获取实例后都要恢复状态的操作。
  6. 在面向对象中,如果有状态共享的需求,可以将共享状态和逻辑封装在一个单例类中。
  7. 单例可以简化代码,从而降低维护成本。在不需要多个实例的情况下,单例可以消除判断逻辑。
  如下是一些伪代码的单例DEMO。
  配置类
  配置类信息在程序运行期间仅需要一个实例,使用单例模式可以保证全局唯一:
  class Config(metaclass=SingletonMetaCls):
      def __init__(self):
          self.config = {'timeout':100, 'port':8000}
  # 访问配置    
  print(Config().config)
  2. 日志类
  日志类也只需要一个实例输出日志信息即可:
  class Logger(metaclass=SingletonMetaCls):
      def __init__(self):
          print("初始化logger")
          
      def log(self, msg):
          print(f"log: {msg}")
          
  # 使用        
  Logger().log("测试日志")
  任务池
  控制任务池的资源个数,只初始化指定数量的连接:
  class TaskPool(metaclass=SingletonMetaCls):
      def __init__(self, size=10):
          print(f"初始化{size}个任务到池中")
          
  # 初始化一个10大小的任务池        
  pool = TaskPool()
  总之,任何只需要一个实例、不保存状态的工具/帮助类,你需要限制实例个数的场景,都可以考虑使用单例模式实现。
  三、单例的实现
  重写 __new__ 方法
  Python 的 new 对象不像java等其他语言一样,一些初学者可能会误认为 __init__ 方法是构造对象,实则不是,__init__方法是初始化对象属性,而__new__ 方法才是真正构造类实例对象。因此我们可以通过 重写__new__方法 并在方法内部添加判断逻辑,来限制一个类只创建一个实例。
  class Singleton(object):
      _instance = None
      def __new__(cls, *args, **kwargs):
          # 重写 __new__ 实现单例
          if not cls._instance:
              cls._instance = super().__new__(cls, *args, **kwargs)
          return cls._instance
      def __init__(self):
          # 初始化实例属性
          self.demo_name = "Singleton Demo"
  s1 = Singleton()
  s2 = Singleton()
  print("s1 demo_name", s1.demo_name)
  print("s2 demo_name", s2.demo_name)
  print("s1", s1)
  print("s2", s2)
  print("s1 is s2", s1 is s2)
  print("s1 == s2", s1 == s2)
  >>> out
  s1 demo_name Singleton Demo
  s2 demo_name Singleton Demo
  s1 <__main__.Singleton object at 0x1051dcb80>
  s2 <__main__.Singleton object at 0x1051dcb80>
  s1 is s2 True
  s1 == s2 True
  通过重写 __new__ 实现单例,就可以实现最简单的单例模式。可以发现创建的对象的内存地址都是一样的。
  上面的实现模式属于懒汉模式,还有一种叫做饿汉模式。先简单介绍下这两种模式概念。
  懒汉模式
  懒汉模式是等到需要才创建实例,比如:
  一个游戏需要读取玩家存档数据的类,如果玩家没有存档,就不需要创建该类的实例,等玩家第一次存档时再实例化该类,读取并保存游戏状态。这种情况下使用懒汉模式更合适,不会提前占用内存资源。
  饿汉模式
  饿汉模式是提前创建实例,比如:
  一个数据库连接池类,系统启动时就需要初始化一个指定大小的连接池,以备后续使用。这里需要饿汉模式提前创建并准备好数据库连接池,否则后面需要数据库连接时会出现延迟。
  ·懒汉是按需创建,节省资源
  · 饿汉是准备实例,避免后续延迟
  饿汉代码实现
  #!/usr/bin/python3
  # -*- coding: utf-8 -*-
  # @Author: Hui
  # @Desc: { 单例DEMO }
  # @Date: 2023/08/22 09:27
  class BaseSingleton(object):
      _instance = None
      @classmethod
      def instance(cls):
          if not cls._instance:
              cls._instance = cls()
          return cls._instance
  class Singleton(BaseSingleton):
      # _instance = Singleton() 这是错误的语法
      def __new__(cls, *args, **kwargs):
          if not cls._instance:
              BaseSingleton.instance()
          return cls._instance
      def __init__(self):
          # 初始化实例属性
          self.demo_name = "Singleton Demo"
  s1 = Singleton.instance()
  s2 = Singleton.instance()
  s3 = Singleton()
  s4 = Singleton()
  print("s1", s1)
  print("s2", s2)
  print("s3", s3)
  print("s4", s4)
  print("s1 is s2", s1 is s2)
  print("s2 is s3", s2 is s3)
  print("s3 is s4", s3 is s4)
  >>> out
  s1 <__main__.BaseSingleton object at 0x1051dcf70>
  s2 <__main__.BaseSingleton object at 0x1051dcf70>
  s3 <__main__.BaseSingleton object at 0x1051dcf70>
  s4 <__main__.BaseSingleton object at 0x1051dcf70>
  s1 is s2 True
  s2 is s3 True
  s3 is s4 True
  python的饿汉模式,不能直接在类中构造自身对象,如下是错误的写法。
  class Singleton(BaseSingleton):
      _instance = Singleton() # 这是错误的语法
  因此这里通过添加一个静态方法 instance() 来实现饿汉单例模式,但感觉有点不太像,就是要在new 对象前先通过 instance() 方法初始化下对象实例,到后面在其他模块使用已经存在的实例即可。但有时候就是想,instance() 是单例,Singleton() 这种new 对象不是,那就不要重写 __new__ 方法即可。因为有时候new对象想重新初始化属性。
  虽然通过重写 __new__ 方法,实现了单例模式,但不够完善,在并发的情况下还是会创建多个实例,属于线程不安全,因此还是需要改造下,这里先展示并发问题,具体改造看下面装饰器的写法。
  class Singleton(object):
      _instance = None
      def __new__(cls, *args, **kwargs):
          # 重写 __new__ 实现单例
          if not cls._instance:
              print("new instance")
              cls._instance = super().__new__(cls, *args, **kwargs)
          return cls._instance
      def __init__(self):
          # 初始化实例属性
          self.demo_name = "Singleton Demo"
  def create_obj(num):
      s = Singleton()
      print(f"s{num}", s)
  # 多线程测试并发
  with ThreadPoolExecutor() as pool:
      for i in range(3, 10):
          pool.submit(create_obj, i)
  测试效果,这个要多运行几遍才有概率复现。
  可以发现,多线程的打印是凌乱的,但已经可以证明有2个线程创建了两实例对象 new instance,对象的内存地址也不一样。这是由于 if not cls._instance: 操作是非原子性操作的导致的并发问题。
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号