Python 设计模式之单例模式

文章目录

单例模式是一种创建型设计模式,其目的是为了保证一个类仅有一个实例,并提供一个访问它的全局访问点,这么做有时是为了限制对共享资源的并发访问。

关于这个模式,我不打算像前面几个创建型模式逐步讲为什么使用,而是关注怎么实现单例 的目的。

单例模式的实现

单例模式有两种实现方式:

  • Module-level Singleton
  • Classic Singleton

Module-level Singleton

通过模块文件的方式实现,一个模块本身就是一个单例。

Modules are "singletons" in Python because import only creates a single copy of each module; subsequent imports of the same name keep returning the same module object.

首先我们的文件结构如下:

.
├── test0.py
├── test2.py
└── utils.py

utils.py 中的代码:

python 复制代码
print("import utils:")
class Man:
    common = {}
    def __init__(self):
        self.shared = {}
    
man = Man()
shared_value = "hello"

test0.py 中的代码:

python 复制代码
import utils

print(utils.shared_value)
utils.shared_value += " add by test0 module"
print(id(utils))
print(id(utils.man))

test2.py 中的代码:

python 复制代码
import utils

print("*"*10)
print(utils.shared_value)
print(id(utils))
print(id(utils.man))

此时在终端中调试

>>> import test0
import utils:
hello
2264080776976
2264081025424

>>> import test2
**********
hello add by test0 module
2264080776976
2264081025424

可以看到在同一个进程中, test0test2 都在代码中执行过import utils,对比输出后可以得到下面的观察结果:

  1. 该模块只被执行一次(这可以从 import test0 的输出import utils获知)
  2. 两个代码中出现的utilsid 是一致的,这也意味着调用它们的属性也会是一致的,比如man 这个 Man 实例
  3. test0 中执行utils.shared_value修改影响到 test2 中的 utils.shared_value

而这就意味着我们用模块文件的形式实现了单例。

Classic Singleton

这是经典的单例实现方式,通过这种方式实现的类只会在没有其他实例存在时,才会创建一个实例,否则它只会返回已经创建的类实例。

相信大家都对 __init__ 很熟悉,但很少接触到 __new__, 而这种单例的实现就是基于这个对这个类型函数的修改。

首先要明白一个类进行实例化时会经过下面的调用流程:
调用 new 创建实例 调用 init 对实例进行初始化

而对 __new__ 的实现中主要两种形式:

  1. 定义一个类级别的字典变量,每次生成实例前判断字典中是否有值
  2. 定义一个类级别的属性,每次生成实例前判断这个属性是否为空或者是否存在

至于再将这种形式实现成装饰器还是单例类 也不过是额外发挥罢了。

python 复制代码
# 利用 object instance
class SingletonClass(object):
    def __new__(cls):
        if not hasattr(cls, "instance"):
            cls.instance = super(SingletonClass, cls).__new__(cls)
        return cls.instance

# 利用字典变量 instance
class SingletonClassVersion2(object):
    instance = {}
    def __new__(cls):
        if "singleton" not in cls.instance:
            cls.instance["singleton"] = super(SingletonClass, cls).__new__(cls)
        return cls.instance["singleton"]

singleton = SingletonClass()
new_singleton = SingletonClass()

print(singleton is new_singleton)
print(id(singleton))
print(id(new_singleton))

但这种实现方式有一个问题:即它虽然阻止了类每一次调用__new__ 新建一个实例,但是它每次都会触发__init__ ,这意味着定义在__init__ 函数中的初始化操作会被重复执行,覆盖掉实例变量之前对实例变量的操作,因此共享数据需要保存在类变量中。

解决这个问题的思路就是修改__init__ 方法:

python 复制代码
class SingletonClass(object):
    instance = None
    initialized = False
    
    def __new__(cls):
        if not cls.instance:
            cls.instance = super(SingletonClass, cls).__new__(cls)
        return cls.instance
    
    def __init__(self):
        if not self.initialized:
            # 这里放置初始化逻辑
            self.initialized = True
            print("Initializing SingletonClass instance")

单例模式实现的优化

线程安全问题

上面的 classic singleton 实现方式在并发程序中可能会出现多个线程同时访问类进行实例化时创建两个实例。为了确保同一时刻只有一个线程可以调用 __new__ ,需要增加锁机制和重复检查。

python 复制代码
from threading import Lock

class SingletonClass(object):
	_lock = Lock()
    def __new__(cls):
	    if not hasattr(cls, "instance"):
            with cls._lock:
                if not hasattr(cls, "instance"):
    	            cls.instance = super(SingletonClass, cls).__new__(cls)
	    return cls.instance

可以看到程序中不但增加锁,还进行 double-check,这样做的原因可以看下面这张流程图,可以看到在 thread2 试图创建实例时,如果不再次进行实例检查(此时thread1 已经创建实例)就会创建又一个实例。
No thread1释放锁 thread1 thread2 instance exists? 争夺锁资源 thread1 抢到 thread2 阻塞 创建实例 thread2 获取到锁资源 试图创建实例

违反单一职责原则

classic singleton的实现方式将维护一个实例的职责和类本身的功能职责组合在一起,这不利于维护。优化这个问题的两种思路:

  1. 作为类装饰器实现
  2. 作为 metaclass 使用

类装饰器的实现

python 复制代码
def singleton(cls):
    instances = {}
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

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

这种方式的好处在于使用灵活并且清晰(只要使用该装饰器就知道这个类是单例)

metaclass方式实现

python 复制代码
class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(SingletonMeta, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

class Wizard(metaclass=SingletonMeta):
    def __init__(self, name):
        self.name = name

单例模式的应用

  • 数据库连接的 connection 管理, 一般会使用一个连接池实例(单例模式),降低每次都要新建连接的开销
  • 常见的Logger 实例,项目中的代码使用一个共同的日志实例
  • 管理配置的实例
相关推荐
Chef_Chen8 分钟前
从0开始机器学习--Day17--神经网络反向传播作业
python·神经网络·机器学习
千澜空27 分钟前
celery在django项目中实现并发任务和定时任务
python·django·celery·定时任务·异步任务
斯凯利.瑞恩34 分钟前
Python决策树、随机森林、朴素贝叶斯、KNN(K-最近邻居)分类分析银行拉新活动挖掘潜在贷款客户附数据代码
python·决策树·随机森林
yannan201903131 小时前
【算法】(Python)动态规划
python·算法·动态规划
蒙娜丽宁1 小时前
《Python OpenCV从菜鸟到高手》——零基础进阶,开启图像处理与计算机视觉的大门!
python·opencv·计算机视觉
光芒再现dev1 小时前
已解决,部署GPTSoVITS报错‘AsyncRequest‘ object has no attribute ‘_json_response_data‘
运维·python·gpt·语言模型·自然语言处理
好喜欢吃红柚子1 小时前
万字长文解读空间、通道注意力机制机制和超详细代码逐行分析(SE,CBAM,SGE,CA,ECA,TA)
人工智能·pytorch·python·计算机视觉·cnn
小馒头学python1 小时前
机器学习是什么?AIGC又是什么?机器学习与AIGC未来科技的双引擎
人工智能·python·机器学习
神奇夜光杯2 小时前
Python酷库之旅-第三方库Pandas(202)
开发语言·人工智能·python·excel·pandas·标准库及第三方库·学习与成长
千天夜2 小时前
使用UDP协议传输视频流!(分片、缓存)
python·网络协议·udp·视频流