Python 设计模式之单例模式

news/2024/10/11 5:30:51/

文章目录


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

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

单例模式的实现

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

  • Module-level Singleton
  • Classic Singleton

Module-level Singleton

  • 官方文档-how-do-i-share-global-variables-across-modules

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

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 utilsprint(utils.shared_value)
utils.shared_value += " add by test0 module"
print(id(utils))
print(id(utils.man))

test2.py 中的代码:

python">import utilsprint("*"*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 = Noneinitialized = Falsedef __new__(cls):if not cls.instance:cls.instance = super(SingletonClass, cls).__new__(cls)return cls.instancedef __init__(self):if not self.initialized:# 这里放置初始化逻辑self.initialized = Trueprint("Initializing SingletonClass instance")

单例模式实现的优化

  • 参考 Exploring the Singleton Design Pattern in Python

线程安全问题

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

python">from threading import Lockclass 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 实例,项目中的代码使用一个共同的日志实例
  • 管理配置的实例

http://www.ppmy.cn/news/1506349.html

相关文章

SQL中插入数据,获取新增数据的id主键

方法一:使用Mybatis plus自带的insert()方法 entity Data AllArgsConstructor NoArgsConstructor TableName("student") public class Student{ApiModelProperty("主键")TableId(type IdType.AUTO)private Integer id;ApiModelProperty(&quo…

Python 爬虫入门(八):爬虫工程化及Scrapy简介「详细介绍」

Python 爬虫入门(八):爬虫工程化及Scrapy简介「详细介绍」 前言1. Python1.1 Python 简介1.2 Python 爬虫的优势1.3 必须掌握的 Python 基础知识1.3.1 基本语法1.3.2. 函数和模块1.3.3 文件操作1.3.4 数据处理1.3.5 类和对象1.3.6 异常处理 2…

网页版IntelliJ IDEA部署

在服务器部署网页 IntelliJ IDEA 引言 大家好,我是小阳,今天要为大家带来一个黑科技——如何在云端部署和使用WEB版的IntelliJ IDEA,让你在任何地方都可以随心所欲地进行Java开发。这个方法特别适合那些用着老旧Windows电脑,部署…

徐州市委书记宋乐伟一行莅临非凸科技徐州分公司调研

7月23日,徐州市委书记宋乐伟一行莅临非凸科技徐州分公司调研,详细了解非凸科技数智交易产品的生态体系以及AI算力赋能的实践成果,并就相关工作进行了现场指导与交流。 非凸科技徐州分公司位于淮海路经济区金融服务中心云盛大厦,致…

centos上传工具

yum install lrzsz 安装完成之后 作用是 输入 rz 可以本地上传文件

ctfhub文件包含

文件包含 url http://challenge-41cbfbe04828b338.sandbox.ctfhub.com:10800/ 构造url,利用hackabar进行Post data修改测试 http://challenge-41cbfbe04828b338.sandbox.ctfhub.com:10800/?fileshell.txt ctfhubsystem("ls"); ctfhubsystem("ls…

jupyter notebook魔法命令

%xmode 魔法命令来控制异常报告: 输入魔法命令:在 IPython 或 Jupyter Notebook 的一个新单元格中,输入以下命令之一来设置异常报告模式: 切换到 Plain 模式(简洁输出): %xmode Plain切换回 Con…

服务器数据恢复—Raid故障导致存储中数据库数据丢失的数据恢复案例

服务器存储数据恢复环境&故障情况: 一台光纤存储中有一组由16块硬盘组成的raid。 该存储出现故障导致数据丢失。RAID中2块盘掉线,还有1块盘smart状态为“警告”。 服务器存储数据恢复过程: 1、通过该存储自带的存储管理软件将当前存储的完…