• Python 设计模式之单例模式



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

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

    单例模式的实现

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

    • Module-level Singleton
    • Classic Singleton

    Module-level Singleton

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

    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 中的代码:

    print("import utils:")
    class Man:
        common = {}
        def __init__(self):
            self.shared = {}
        
    man = Man()
    shared_value = "hello"
    

    test0.py 中的代码:

    import utils
    
    print(utils.shared_value)
    utils.shared_value += " add by test0 module"
    print(id(utils))
    print(id(utils.man))
    

    test2.py 中的代码:

    import utils
    
    print("*"*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. 定义一个类级别的属性,每次生成实例前判断这个属性是否为空或者是否存在

    至于再将这种形式实现成装饰器还是单例类 也不过是额外发挥罢了。

    # 利用 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__ 方法:

    class SingletonClass(object):
        instance = None
        initialized = False
        
        def __new__(cls):
            if not cls.instance:
                cls.instance = super(SingletonClass, cls).__new__(cls)
            return cls.instance
        
        def __init__(self):
            if not self.initialized:
                # 这里放置初始化逻辑
                self.initialized = True
                print("Initializing SingletonClass instance")
    

    单例模式实现的优化

    线程安全问题

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

    from threading import Lock
    
    class 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 使用

    类装饰器的实现

    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方式实现

    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 实例,项目中的代码使用一个共同的日志实例
    • 管理配置的实例
  • 相关阅读:
    全息干涉图补零尺寸与三种重构方法重建像间的关系研究
    01标定相关理论
    Java重写与重载
    svn log 高级命令解释
    Leetcode(76)——最小覆盖子串
    Django在Views视图内取消当前请求的@receiver(post_save, sender=xxxxxx)信号
    目标检测网络YOLO进化之旅
    MATLAB算法实战应用案例精讲-【数模应用】K近邻算法(KNN)(附MATLAB、Python、R语言和Java代码)
    L. Lemper Cooking Competition(前缀和/逆序对/树状数组/归并排序)
    解决github ping不通的问题(1024程序员节快乐!
  • 原文地址:https://blog.csdn.net/yeshankuangrenaaaaa/article/details/141001280