单例模式坑这么多,不会用别乱用
创始人
2024-04-11 02:58:35
0

单例模式一种常见的设计模式,并且也是最基础的设计模式,需要每个开发人员都熟练掌握。

当你希望在整个系统中,某个类只能出现一个实例时,就需要学会使用单例模式。

比较常见的场景是:某个项目的配置信息存放在一个配置文件中,通过一个 Config 的类来读取配置文件的信息。如果在程序运行期间,有很多地方都需要使用配置文件的内容,也就是说,很多地方都需要创建 Config 对象的实例,这就导致系统中存在多个 Config 的实例对象,而这样会严重浪费内存资源,尤其是在配置文件内容很多的情况下。事实上,类似 Config 这样的类,我们希望在程序运行期间只存在一个实例对象。

在 Python 中,可以用多种方法实现单例模式,常见的有:

  • 通过使用模块

  • 通过装饰器

  • 通过__new__方法

  • 通过元类

不同的方法,有不同的坑,对 Python 机制认识不够的同学,很容易踩到坑,本文用实例来详细说明一下这几种方法的区别,以及那些不容易被察觉的坑点,给出最佳的选择方案。

文章目录

    • 技术提升
    • 1. 通过使用模块
    • 2. 通过装饰器
    • 3. 通过__new__方法
    • 4. 通过 metaclass
    • 5. 总结一下

技术提升

项目代码、数据、技术交流提升,均可加交流群获取,群友已超过2000人,添加时最好的备注方式为:来源+兴趣方向,方便找到志同道合的朋友

方式①、添加微信号:dkl88191,备注:来自CSDN
方式②、微信搜索公众号:Python学习与数据挖掘,后台回复:加群

下面就详细说明以下这几种实现方法:

1. 通过使用模块

在Python中,Python 的模块就是天然的单例模式。

因为模块在第一次导入时,会生成 .pyc 文件,当第二次导入时,就会直接加载 .pyc 文件,而不会再次执行模块代码。因此,我们只需把相关的函数和数据定义在一个模块中,就可以获得一个单例对象了。

看如下示例:

# 例如在config.py文件中定义Config类和实例
class Config:LOG_LEVEL = 'INFO'...config = Config()# 然后在其他文件中,通过import导入此实例
from config import configprint(config.LOG_LEVEL)
# 输出结果:INFO

如上所示,通过在文件中先生成一个类实例,然后在别的文件中直接导入这个类实例,就实现了单例模式。

2. 通过装饰器

另外一种典型的用法是使用类装饰器,由于逻辑比较易懂,被广泛被开发人员使用。

如下是一个例子,在每次对 Config 实例化的时候,都会进入 Singleton 类装饰器,该装饰器维护一个 dict,key 为 cls,value 为 instance,每次实例化都检查该 cls 是否已经在 dict 中存在,若存在则直接将之前实例化的对象返回,如此来保证单例。

from functools import wrapsdef Singleton(cls):instance = {}@wraps(cls)def wrapper(*args, **kwargs):if cls not in instance:instance[cls] = cls(*args, **kwargs)return instance[cls]return wrapper@Singleton
class Config(object):passcfg1 = Config()
cfg2 = Config()print(cfg1 == cfg2)

运行后,输出结果 True

不过,这种方法虽然逻辑非常清晰易懂,但却有一个非常大的问题:经过 Singleton 装饰的后,其返回的是函数,因为无法被继承。

装饰器由于本身机制的限制,在实例单例的同时也带来了一定的 “副作用”。

仔细一想,用装饰器实现单例的思路,不就是拦截实例对象的创建,来保证类只有唯一的实例嘛!

那我们只要换种方法,来拦截实例对象的创建不就行了。

通常的方法,有两种:

  • __new__ 方法

  • 元类

3. 通过__new__方法

先说一下 __new__ 方法,它是 Python 中的魔法方法之一。

很多人可能对他不是很熟悉,不过完全没有关系,你只要知道,当你在实例化的时候,是先执行类的 __new__ 方法,再执行类的 __init__ 即可。

因此,我们可以在 __new__ 上做一些事情,使得类的实例只能存在一个。

如下是一个示例,在第一次实例的时候,会在 cls 上添加一个属性 _instance 来保存第一个实例,后面再实例化时,就会返回第一次创建的时候,

class Config(object):def __new__(cls, *args, **kwargs):if not hasattr(cls, '_instance'):cls._instance = super().__new__(cls, *args, **kwargs)return cls._instancecfg1 = Config()
cfg2 = Config()print(cfg1 == cfg2)

运行后,输出结果 True

细心的朋友,想必已经发现,如果有其他类继承了 Config 这个类,那么 _instance 也同样会被子类继承过去,这会导致只要 Config 及其子类只能有一个实例,只要 Config 实例过了,其子类就不能再实例化了,但这显然不是我们所期望的。

有的朋友,可能会想到用双下划线的 __instance,这样就不会被继承了。

很遗憾的是,双下划线的属性,虽然不会被继承,但却有一个问题,就是属性名会被 Python 修改掉,变成 _Config__instance,这样一样,我们在编写 __new__ 时就无法使用 hasattr 来判断。

class Config(object):def __new__(cls, *args, **kwargs):if not hasattr(cls, '__instance'):cls.__instance = super().__new__(cls, *args, **kwargs)return cls.__instancecfg1 = Config()
print(hasattr(Config, "__instance"))          # 输出:False
print(hasattr(Config, "_Config__instance"))   # 输出:True

最好的做法是:在所有的子类将 _instance 重置为 None,并且重写 __new__ 方法,最重要的是不要去调用 supper.__new__()

class Config(object):def __new__(cls, *args, **kwargs):if not hasattr(cls, '_instance'):cls._instance = super().__new__(cls, *args, **kwargs)return cls._instanceclass ConfigExt(Config):_instance = Nonedef __new__(cls, *args, **kwargs):if not hasattr(cls, '_instance'):cls._instance = object.__new__(cls, *args, **kwargs)return cls._instancecfg1 = Config()
cfg2 = ConfigExt()
cfg3 = ConfigExt()print(cfg1 == cfg2)  # False
print(cfg2 == cfg3)  # True

可以看到,通过修改 __new__ 方法,已经可以解决装饰器不能被继承的问题,但在有可能被继承的类里,却存在安全隐患。

4. 通过 metaclass

metaclass 是什么呢?用一句话说明

  • 类,是用来创建实例对象的「模板」。

  • 而元类,是创建类的「模板」。

因此我们可以通过修改元类,来使得创建此类时,就直接是一个单例类。

学习元类的作用过程,可以用普通类来做对比学习:

  • 在创建一个实例时,会走类的 __new__ 方法

  • 同样地逻辑,在创建一个普通类时,也会走元类的 __new__

与此同时:

  • 在类里实现了 __call__ 可以让这个类的实例,变成可调用对象

  • 对普通类进行实例化时,实际是对一个元类的实例(也就是普通类)进行直接调用,因此会走元类的 __call__ 方法

如下是一个示例,可以发现,父类和子类都可以完美实现单例,而不需要有额外的约定,也不需要团队里的人有统一的技术积累,就像平时一样,不会有任何突兀的感觉。

class Singleton(type):def __init__(cls, *args, **kwargs):cls.__instance = Nonesuper().__init__(*args, **kwargs)def __call__(cls, *args, **kwargs):if not cls.__instance:cls.__instance = super().__call__(*args, **kwargs)return cls.__instanceclass Config(metaclass=Singleton):passclass ConfigExt(Config):passcfg1 = Config()
cfg2 = Config()
cfg3 = ConfigExt()
cfg4 = ConfigExt()print(cfg1 == cfg2)  # True
print(cfg2 == cfg3)  # False
print(cfg3 == cfg4)  # True

另外,该方法在多线程场景下并发创建实例对象时,由于初始化时,是需要一点时间,那么就会导致数据不同步的问题,导致出现多个实例。

为了方便复现,我在 __call__ 里加了 time.sleep(1) 来实现延长实例化的时间

import time
import threadingclass Singleton(type):def __init__(cls, *args, **kwargs):cls.__instance = Nonesuper().__init__(*args, **kwargs)def __call__(cls, *args, **kwargs):if not cls.__instance:time.sleep(1)cls.__instance = super().__call__(*args, **kwargs)return cls.__instanceclass Config(metaclass=Singleton):passdef task():cfg = Config()print(id(cfg))for i in range(10):t = threading.Thread(target=task)t.start()

运行结果如下,说明单例模式出现了问题

4336068992
4373051424
4335324896
4336069088
4335324848
4336069088
4335324896
4336069088
4335324848
4336069088

但这个问题可以通过线程锁来解决,先定义一个装饰器 synchronized ,然后在元类里的 __call__ 加上这个装饰器。

import time
import threadingdef synchronized(func):func.__lock__ = threading.Lock()def lock_func(*args, **kwargs):with func.__lock__:return func(*args, **kwargs)return lock_funcclass Singleton(type):def __init__(cls, *args, **kwargs):cls.__instance = Nonesuper().__init__(*args, **kwargs)@synchronizeddef __call__(cls, *args, **kwargs):if not cls.__instance:time.sleep(1)cls.__instance = super().__call__(*args, **kwargs)return cls.__instanceclass Config(metaclass=Singleton):passdef task():cfg = Config()print(id(cfg))for i in range(10):t = threading.Thread(target=task)t.start()

运行的结果就正常了

4381387136
4381387136
4381387136
4381387136
4381387136
4381387136
4381387136
4381387136
4381387136
4381387136

5. 总结一下

在 Python 中有很多种方法实现单例的效果,但不同的方法,却有不同的局限性:

  • 使用模块:最简单直接且安全,推荐使用

  • 使用装饰器:不能被继承,不推荐使用

  • 使用 __new__ 方法:需要开发成员对Python有足够的认识,不然代码会有BUG。

  • 使用 metaclass:完美的单例实践,也推荐使用,但要注意加锁

相关内容

热门资讯

StarRocks 集群安装部... 下表为规划的集群组件分配 域名starrocks1starrocks2starrocks3组件my...
跨平台freebasic集锦(... 目录dim例子语法一行多个变量共享 dim例子 语法 一行多个变量 dim x as integer...
TypeScript 中文手册... 目录 基础类型 介绍 布尔值 数字 字符串 数组 元组 Tuple 枚举 任意值 空值 Null ...
【蓝桥杯】历届真题 天干地支(... 【资源限制】         内存限制:256.0MB   C/C++...
多模太大模型清单收集 AI大一统:阿里达摩院发布多任务、多模态统一模型OFA功能:包括3类跨模...
js单线程及异步笔记 js单线程及异步笔记 js运行时默认是单线程的,除非显式的使用或创建了其他线程 浏览器...
基于 SSH 的视频教学平台 完整代码:https://download.csdn.net/download/qq_3873501...
1005.K次取反后最大化的数... 1005.K次取反后最大化的数组和 我是暴力算出来的,看题解后知道了新的解法。 本题求...
使用JavaScript控制H... 使用JavaScript控制HTML元素的显示和隐藏利用来JS控制页面控件显示和隐藏有四种方法&#x...
实现数字到Excel中列序号的... 关键字:Python,Excel,ChatGPT的代码 最...