• 单例模式坑这么多,不会用别乱用


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

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

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

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

    • 通过使用模块

    • 通过装饰器

    • 通过__new__方法

    • 通过元类

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

    技术提升

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

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

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

    1. 通过使用模块

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

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

    看如下示例:

    # 例如在config.py文件中定义Config类和实例
    class Config:
        LOG_LEVEL = 'INFO'
        ...
    
    config = Config()
    
    
    # 然后在其他文件中,通过import导入此实例
    from config import config
    
    print(config.LOG_LEVEL)
    # 输出结果:INFO
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

    2. 通过装饰器

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

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

    from functools import wraps
    
    
    def 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):
        pass
    
    
    cfg1 = Config()
    cfg2 = Config()
    
    print(cfg1 == cfg2)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    运行后,输出结果 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._instance
    
    
    cfg1 = Config()
    cfg2 = Config()
    
    print(cfg1 == cfg2)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    运行后,输出结果 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.__instance
    
    
    cfg1 = Config()
    print(hasattr(Config, "__instance"))          # 输出:False
    print(hasattr(Config, "_Config__instance"))   # 输出:True
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    最好的做法是:在所有的子类将 _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._instance
    
    
    class ConfigExt(Config):
        _instance = None
    
        def __new__(cls, *args, **kwargs):
            if not hasattr(cls, '_instance'):
                cls._instance = object.__new__(cls, *args, **kwargs)
            return cls._instance
    
    
    cfg1 = Config()
    cfg2 = ConfigExt()
    cfg3 = ConfigExt()
    
    print(cfg1 == cfg2)  # False
    print(cfg2 == cfg3)  # True
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

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

    4. 通过 metaclass

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

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

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

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

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

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

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

    与此同时:

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

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

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

    class Singleton(type):
        def __init__(cls, *args, **kwargs):
            cls.__instance = None
            super().__init__(*args, **kwargs)
    
        def __call__(cls, *args, **kwargs):
            if not cls.__instance:
                cls.__instance = super().__call__(*args, **kwargs)
            return cls.__instance
    
    
    class Config(metaclass=Singleton):
        pass
    
    
    class ConfigExt(Config):
        pass
    
    
    cfg1 = Config()
    cfg2 = Config()
    cfg3 = ConfigExt()
    cfg4 = ConfigExt()
    
    print(cfg1 == cfg2)  # True
    print(cfg2 == cfg3)  # False
    print(cfg3 == cfg4)  # True
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

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

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

    import time
    import threading
    
    class Singleton(type):
        def __init__(cls, *args, **kwargs):
            cls.__instance = None
            super().__init__(*args, **kwargs)
    
        def __call__(cls, *args, **kwargs):
            if not cls.__instance:
                time.sleep(1)
                cls.__instance = super().__call__(*args, **kwargs)
            return cls.__instance
    
    
    class Config(metaclass=Singleton):
        pass
    
    
    def task():
        cfg = Config()
        print(id(cfg))
    
    
    for i in range(10):
        t = threading.Thread(target=task)
        t.start()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

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

    4336068992
    4373051424
    4335324896
    4336069088
    4335324848
    4336069088
    4335324896
    4336069088
    4335324848
    4336069088
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    import time
    import threading
    
    
    def synchronized(func):
        func.__lock__ = threading.Lock()
    
        def lock_func(*args, **kwargs):
            with func.__lock__:
                return func(*args, **kwargs)
    
        return lock_func
    
    
    class Singleton(type):
        def __init__(cls, *args, **kwargs):
            cls.__instance = None
            super().__init__(*args, **kwargs)
    
        @synchronized
        def __call__(cls, *args, **kwargs):
            if not cls.__instance:
                time.sleep(1)
                cls.__instance = super().__call__(*args, **kwargs)
            return cls.__instance
    
    
    class Config(metaclass=Singleton):
        pass
    
    
    def task():
        cfg = Config()
        print(id(cfg))
    
    
    for i in range(10):
        t = threading.Thread(target=task)
        t.start()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    运行的结果就正常了

    4381387136
    4381387136
    4381387136
    4381387136
    4381387136
    4381387136
    4381387136
    4381387136
    4381387136
    4381387136
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    5. 总结一下

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

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

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

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

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

  • 相关阅读:
    基于vue3和element-plus的省市区级联组件
    ninja编译方法介绍
    剑指 Offer 40. 最小的k个数【查找排序】
    函数式编程中元组的简单运用
    使用RCurl和R来爬虫视频
    单片机-控制按键点亮LED灯
    C++ - 一些特殊类的设计
    京东面试:MQ 消息丢失、重复、积压问题,如何解决?
    Meta分析核心技术
    【flink】RowData copy/clone方式
  • 原文地址:https://blog.csdn.net/m0_59596937/article/details/127973364