当前位置: 首页 > news >正文

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

目录

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

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

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

四. 类装饰器

五. 重写__new__方法

六. 元类

七. 总结


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

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

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

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

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

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

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

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

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

class SingletonError(Exception):'''不能为单例类创建多个实例'''class 孤狼:_instance = Nonedef __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# 必须完全使用这个方法来获取实例@classmethoddef 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 wrapsdef 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 wrapsdef 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 = Nonedef __new__(cls, *args, **kwargs):if not cls._instance:cls._instance = super().__new__(cls)return cls._instancedef __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 = Nonedef __new__(cls, *args, **kwargs):if not cls._instance:cls._instance = super().__new__(cls)return cls._instancedef __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 = Noneif cls not in cls._instances:cls._instances = super().__call__(*args, **kwargs)return cls._instances

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

七. 总结

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

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

相关文章:

  • 防爆手机VS普通手机,区别在哪里?
  • 获取oracle的HQL日志,采取参数日志,拼装SQL语句
  • Oracle初识
  • Java大师成长计划之第32天:使用Kubernetes进行Java应用编排与管理
  • C++学习-入门到精通【9】面向对象编程:继承
  • 低空经济数据湖架构设计方案
  • 贝壳后端golang面经
  • 浅浅学:XCP协议原理及应用
  • ctf.show pwn入门 堆利用-前置基础 pwn142
  • STM32 Keil工程搭建 (手动搭建)流程 2025年5月27日07:42:09
  • Excel常用公式全解析(1):从基础计算到高级应用
  • STM32之FreeRTOS移植(重点)
  • 6.4.5_关键路径
  • 尚硅谷redis7 49-51 redis管道之理论简介
  • C++学习提问
  • day05-常用API(二):Lambda、方法引用详解
  • 洛谷 P3372 【模板】线段树 1
  • [学习]C语言指针函数与函数指针详解(代码示例)
  • 001 flutter学习的注意事项及前期准备
  • 商城前端监控体系搭建:基于 Sentry + Lighthouse + ELK 的全链路监控实践
  • 如果网站没有做icp备案吗/网站怎么做推广
  • 网站建设的素材/雅虎搜索引擎首页
  • 如何套模板做网站/培训seo
  • 加强政府门户网站建设与管理/武汉seo招聘信息
  • 网站开发文献综述范文/百度竞价推广流程
  • 建设通官方网站下载/谷歌搜索优化