实现单例模式的6种方法(Python)

news2025/7/18 17:37:41

目录

一. 基于模块的实现(简单,易用)

 二. 重新创建时报错(不好用)

三. 只靠方法获取实例(不好用)

四. 类装饰器

五. 重写__new__方法

六. 元类

七. 总结


单例模式(Singleton Pattern)是一种设计模式,其核心目标是确保一个类只有一个实例存在,并提供一个全局访问点。这种模式在需要控制资源访问,节省系统资源或确保全局一致性的场景中非常有用。下面谈一谈Python中6种实现单例的方案,复杂程度基本上是由易到难的。除了第一种方案,剩下的在多线程环境中都有风险。如果你需要多线程单例,请注意加锁!

一. 基于模块的实现(简单,易用)

模块只有在第一次导入时会被初始化,后续导入直接使用已加载的模块。这让Python模块成为了天然的单例,借助它即可轻松获取一个唯一实例:

class _Singleton:
    '''一个不开放的单例类'''
    def __init__(self):
        self._value = '俺是单例'
    
singleton = _Singleton()

然后,需要用到这个单例的地方直接导入现成的实例:

from module import singleton

x = singleton
y = singleton
# x 和 y 是同一个对象吗?
print(x is y)  # True

像math.pi,math.e等的单例效果就是依此实现的。 

 二. 重新创建时报错(不好用)

我们可以自己造一个异常来拒绝多次创建实例,当然不造用现成的也可以。比如:

class SingletonError(Exception):
    '''不能为单例类创建多个实例'''

class 孤狼:
    _instance = None
    def __init__(self, age):
        self.age = age
        # 第一次创建实例时,_instance 为 None,不报错。
        # 第二次创建实例时,_instance 不为 None,直接报错。
        if self.__class__._instance is not None:
            raise SingletonError('爷是孤狼,一山不容二虎!')
        self.__class__._instance = self

狼大 = 孤狼(5)
狼二 = 孤狼(4)

在这个世界里,不能存在狼二,更别说光头弱了:

 不过,其实新的实例已经被创造出来了。只是在初始化的时候强制程序报错,把这个对象直接“扼杀在摇篮中”了,没能赋值给“狼二”。而且,这种方法就怕人家把异常捕获了,那后面会发生什么就不是我们能预测的了。

三. 只靠方法获取实例(不好用)

在这种方案下,我们必须摒弃传统的实例创建方法,转而利用一个类方法获取实例。

class 孤狼:
    def __init__(self, age):
        self.age = age
    # 必须完全使用这个方法来获取实例
    @classmethod
    def get_instance(cls, age):
        # 如果没有实例化过,就创建一个实例
        if not hasattr(cls, '_instance'):
            cls._instance = cls(age)
        # 如果已经创建过实例,就返回这个实例
        return cls._instance

狼大 = 孤狼.get_instance(5)
狼二 = 孤狼.get_instance(4)
print(狼二.age) # 5,而不是4
print(狼大 is 狼二) # True

这里并没有真正拒绝像 "孤狼(参数)" 这样的调用方式,要想完全拒绝这种调用,就绕回第二种方案了。因此这种方法又鸡肋又不好用。

你可能会觉得:方案二,三是在搞笑吗?嗯……这种活儿确实不应该用初级编程方法来干,下面我们看剩下的用元编程技巧实现的三种方案。

四. 类装饰器

这种方案是用一个工厂函数取代原来的类,直接看实现方式。不过要说明一下,如果要实现单例的类是不可哈希的,就要把使用的键从类本身改为类名。不过我没有这么干,因为单例一般就是不可变的。

from functools import wraps

def singleton(cls):
    _instances = {}
    @wraps(cls)
    def wrapper(*args, **kwargs):
        if cls not in _instances:
            _instances[cls] = cls(*args, **kwargs)
        return _instances[cls]
    return wrapper

@singleton
class 孤狼:
    def __init__(self, age):
        self.age = age
@singleton
class 圆头耄耋:
    def __init__(self):
        self.标志技能 = '哈气'

狼大 = 孤狼(5)
狼二 = 孤狼(4)
print(狼二.age) # 5,而不是4
print(狼大 is 狼二) # True

猫爹 = 圆头耄耋()
猫爷 = 圆头耄耋()
print(猫爹 is 猫爷) # True

下面解释一下这个类装饰器。

from functools import wraps

def singleton(cls): #(1)
    _instances = {} #(2)
    @wraps(cls) #(3)
    def wrapper(*args, **kwargs): #(4)
        if cls not in _instances:
            _instances[cls] = cls(*args, **kwargs)
        return _instances[cls]
    return wrapper #(1)

(1):这个类装饰器以@singleton使用,就相当于编写了 cls = singleton(cls)。会将返回的内层函数赋值给类,让类成为内层函数的引用。

(2):这个_instances字典在内层函数的闭包空间内,内层函数可以直接操作它。

(3):就算是单例,使用@wraps保存元数据也是个好习惯!

(4):内层函数现在“夺舍”了类,接受任意参数。如果类不在_instances中,说明还没有为它创建实例,那就创建一个放到_instances中,最后返回的是_instances中的实例。如果不是首次创建,if条件检查就不会通过,最终返回的是第一次创建的实例。

也可以给类新填一个类属性存储实例,后面元类方案我会展示这两种不同的实现策略。这应该是三种元编程方案中最好的,__new__不够灵活,元类太深奥。

五. 重写__new__方法

__new__方法掌管实例的创建,而不是__init__。更具体地,实例先由__new__创建,然后,如果创建的东西确实是本类的实例,就作为self传给__init__进行一系列属性的赋值,完成初始化。如果不是本类的实例(真的可以这样),就不交给__init__。

不过,实现单例只需要确保每次获取的是同一个实例即可,不用担心“生的孩子不是自己的”。下面看具体实现方法:

class 孤狼:
    _instance = None
    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super().__new__(cls)
        return cls._instance
    def __init__(self, age):
        self.age = age

狼大 = 孤狼(5)
狼二 = 孤狼(4)
print(狼大.age)  # 4
print(狼二.age)  # 4
print(狼大 is 狼二)  # True

这种使用_instance的技巧我们已经见过多次了,我就不多解释了。创建实例是委托超类完成的,也就是super().__new__(cls),不需要传入其他参数——这些参数其实就是__init__那里的参数,只不过“生自家孩子”往往用不到罢了。

那为什么这次反而是狼大的age被狼二覆盖了?因为属性age是在__init__中进行赋值的,创建狼二时是最后一次赋值,赋的值是4,所以这个单例的age值从5变成了4。

想要拒绝这种行为,可以在__init__中新增一个条件判断,这时就又是狼大强压狼二了:

class 孤狼:
    _instance = None
    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super().__new__(cls)
        return cls._instance
    def __init__(self, age):
        # 如果没有设置name属性,则设置它
        if not hasattr(self, 'age'):
            self.age = age

狼大 = 孤狼(5)
狼二 = 孤狼(4)
print(狼大.age)  # 5
print(狼二.age)  # 5
print(狼大 is 狼二)  # True

使用__new__其实很不灵活,对子类的支持不足——基本必须重写子类的__new__方法。相比而言,类装饰器和元类就能轻松支持任何类。

六. 元类

一般地,有其他方案我们就不会动用元类,一切类都是元类的实例,它是Python的“终极武器”和“黑魔法”。一来元类相对而言太高深了,二来元类的接口不一定就比其他方案好使。我就觉得类装饰器超级好用呀!如果要在三种元编程方案中选一个,我肯定会选类装饰器。

元类强大到可以干涉类的创建,初始化,和实例化三个过程。分别依赖元类的__new__,__init__,和__call__方法。现在我们想要插手实例创建的逻辑,应该在__call__上下功夫。

你会发现,下面的元类方案和类装饰器方案很类似,都是在外部存储了一个字典:

class MetaSingleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in MetaSingleton._instances:
            MetaSingleton._instances[cls] = super().__call__(*args, **kwargs)
        return MetaSingleton._instances[cls]

class 孤狼(metaclass=MetaSingleton):
    def __init__(self, age):
        self.age = age

狼大 = 孤狼(5)
狼二 = 孤狼(4)
print(狼大.age) # 5
print(狼二.age) # 5
print(狼大 is 狼二) # True

只不过,元类中的实例要委托超类type的__call__来创建,也就是super().__call__(*args, **kwargs)。使用元类的话,在定义类时指定metaclass=……就好了。

也可以把单例存储在类属性中,像下面这样:

class MetaSingleton(type):
    def __call__(cls, *args, **kwargs):
        if not hasattr(cls, '_instances'):
            cls._instances = None
        if cls not in cls._instances:
            cls._instances = super().__call__(*args, **kwargs)
        return cls._instances

第一种方式缺点是占用的空间可能更大,而第二种方式缺点是给类新添了一个属性,在做元编程时可能导致意外发生。 

七. 总结

推荐使用模块单例类装饰器,方案二,三就是来搞笑的,剩下的两种方案的话——__new__更复杂且不够灵活好用;动用元类实现单例完全没必要。只有非常少数的情况是非用元类不可的,我们对元类的态度往往是能不用就不用。如果你感兴趣,我这里有一个真正需要元类出马的简单案例:

利用元类优化装饰器接口的方案https://blog.csdn.net/2402_85728830/article/details/148046472

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2387730.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度学习优化器相关问题

问题汇总 各类优化器SGDMomentumNesterovAdagardAdadeltaRMSpropAdam优化器 为什么Adam不一定最优而SGD最优的深度网络中loss除以10和学习率除以10等价吗L1,L2正则化是如何让模型变得稀疏的,正则化的原理L1不可导的时候该怎么办梯度消失和梯度爆炸什么原因&#xff…

【免费】【无需登录/关注】度分秒转换在线工具

UVE Toolbox 功能概述 这是一个用于地理坐标转换的在线工具,支持两种转换模式: 十进制度 → 度分秒 度分秒 → 十进制度 使用方法 十进制度转度分秒 在"经度"输入框中输入十进制度格式的经度值(例如:121.46694&am…

常见的垃圾回收算法原理及其模拟实现

1.标记 - 清除(Mark - Sweep)算法: 这是一种基础的垃圾回收算法。首先标记所有可达的对象,然后清除未被标记的对象。 缺点是会产生内存碎片。 原理: 如下图分配一段内存,假设已经存储上数据了 标记所有…

力扣面试150题--完全二叉树的节点个数

Day 51 题目描述 思路 根据完全二叉树的规律,完全二叉树的高度可以直接通过不断地访问左子树就可以获取,判断左右子树的高度: 1. 如果相等说明左子树是满二叉树, 然后进一步判断右子树的节点数(最后一层最后出现的节点必然在右子树中) 2. 如…

BLIP3-o:一系列完全开源的统一多模态模型——架构、训练与数据集

摘要 在近期关于多模态模型的研究中,将图像理解与生成统一起来受到了越来越多的关注。尽管图像理解的设计选择已经得到了广泛研究,但对于具有图像生成功能的统一框架而言,其最优模型架构和训练方案仍有待进一步探索。鉴于自回归和扩散模型在…

DNS解析流程入门篇

一、DNS 解析流程 1.1 浏览器输入域名 当在浏览器中输入 www.baidu.com 时,操作系统会按照以下步骤进行 DNS 解析: 检查本地 hosts 文件 :操作系统先检查本地的 /etc/hosts 文件,查看是否存在域名与 IP 地址的对应关系。如果找到…

大模型系列22-MCP

大模型系列22-MCP 玩转 MCP 协议:用 Cline DeepSeek 接入天气服务什么是 MCP?环境准备:VScode Cline DeepSeek**配置 DeepSeek 模型:****配置 MCP 工具****uvx是什么?****安装 uv(会自动有 uvx 命令&…

【监控】Prometheus+Grafana 构建可视化监控

在云原生和微服务架构盛行的今天,监控系统已成为保障业务稳定性的核心基础设施。作为监控领域的标杆工具,Prometheus和Grafana凭借其高效的数据采集、灵活的可视化能力,成为运维和开发团队的“标配”。 一、Prometheus Prometheus诞生于2012…

RAGFlow源码安装操作过程

RAGFlow是一款基于深度文档理解构建的开源 RAG&#xff08;Retrieval-Augmented Generation&#xff09;引擎&#xff0c;可作为Dify的外部知识库使用[1]。本文主要介绍RAGFlow前端和后端等源码安装操作过程。 一.后端安装 特别注意&#xff1a;python ">3.12,<3…

Unity使用XCharts动态配置数据——折线图(LineChart)

XCharts官网地址&#xff1a;https://xcharts-team.github.io/ 本地上传资源&#xff1a;https://download.csdn.net/download/m0_64375864/90919669 效果图&#xff1a; 动态配置数据&#xff1a; public class Test3 : MonoBehaviour {public LineChart lineChart;public …

【HITCSAPP 哈工大计算机系统期末大作业】 程序人生-Hello’s P2P

计算机系统 大作业 题 目 程序人生-Hello’s P2P 专 业 计算机与电子通信类 学   号 2023112915 班   级 23L0505 学 生 杨昕彦 指 导 教 师 刘宏伟 计算机科学…

DAY9 热力图和箱线图的绘制

浙大疏锦行 学会了绘制两个图&#xff1a; 热力图&#xff1a;表示每个特征之间的影响&#xff0c;颜色越深数值越大表示这两个特征的关系越紧密 箱线图&#xff1a;表示每个特征的数据分布情况 箱体&#xff08;Box&#xff09;&#xff1a; 箱体的上下边界分别表示第一四分位…

解释一下NGINX的反向代理和正向代理的区别?

大家好&#xff0c;我是锋哥。今天分享关于【解释一下NGINX的反向代理和正向代理的区别?】面试题。希望对大家有帮助&#xff1b; 解释一下NGINX的反向代理和正向代理的区别? NGINX的反向代理和正向代理的区别主要体现在它们的功能和使用场景上。下面我会详细解释它们的定义…

数学笔记一:标量、向量和矩阵基本概念辨析

一、标量 标量&#xff08;Scalar&#xff09; 是一种仅用数值大小&#xff08;即 “量值”&#xff09;就能完全描述的物理量或数学对象&#xff0c;它不具有方向属性。 例如在实数领域的正数、负数。 在物理学领域的多少斤、多少公斤、水温多少度、气温多少度都是标量。 …

vue3获取两个日期之间的所有时间

1.获取两个日期之间所有年月日 如图所示&#xff1a; 代码如下&#xff1a; <template><div class"datePicker"><el-date-pickerv-model"value1"type"daterange"range-separator"至"start-placeholder"开始时间…

Python 实现简易版的文件管理(结合网络编程)

目录 一、Python 代码实现1. 服务器端2. 客户端 二、结果展示1. 查看当前路径下的内容 ls2. 切换当前路径 cd3. 查看当前路径 pwd4. 显示根目录下的树状结构 tree5. 在当前路径下创建目录 mkdir6. 删除当前路径下的文件或目录 rm7. 复制文件 mv8. 移动文件 cp9. 用户从当前路径…

PXC集群

PXC集群 一、环境介绍二、PXC安装1、关闭默认mysql模块2、安装yum源3、准备pxc安装环境4、安装pxc5、启动mysql&#xff0c;并更改root密码 三、搭建PXC集群1、编辑/etc/my.cnf 配置文件&#xff08;1&#xff09;pxc1节点配置文件&#xff08;2&#xff09;pxc2节点配置文件&a…

线程安全问题的成因

前言 大家晚上好呀~~ 今天学习了线程不安全问题的成因。线程安全问题是十分重要的知识点&#xff0c;我想把我所学的与大家分享一波&#xff0c;希望可以帮助到有需要的人&#xff0c;同时加深自己对于线程安全问题的理解。 分析过程如下 结语 今天心情还不错~ 要坚持持续…

零基础远程连接课题组Linux服务器,安装anaconda,配置python环境(换源),在服务器上运行python代码【3/3 适合小白,步骤详细!!!】

远程连接服务器 请查阅之前的博客——零基础远程连接课题组Linux服务器&#xff0c;安装anaconda&#xff0c;配置python环境&#xff08;换源&#xff09;&#xff0c;在服务器上运行python代码【1/3 适合小白&#xff0c;步骤详细&#xff01;&#xff01;&#xff01;】&am…

unity实现wasd键控制汽车漫游

1.给汽车模型添加Box Collider和Rigidbody 2.创建脚本CarController并加载到汽车模型上 using UnityEngine; using UnityEngine.UI;public class CarController : MonoBehaviour