单例模式是常见的一种设计模式,python中实现单例模式的方法也有很多,本文总结了个人对常见单例模式实现的理解。由于初学python,水平有限,如果理解存在错误,欢迎大家评论指正~

一、通过内部类形式

将要实现单例的类放到内部类中,通过外部类变量_instance来存储,多次实例化外部类时,类变量_instance保持不变,从而实现单例。

class Foo():
    class __A():
        '''
        真正要实现单例的类,对外隐藏
        '''
        def __init__(self,name,age):
            self.name=name
            self.age=age
        def display(self):
            return id(self)
    # 类变量_instance有点类似java中的静态变量。当f1=Foo(),类初始化后,该变量被赋值为__A的实例:__A()
    def __init__(self,*args,**kw):
        if not hasattr(self,'_instance'):
            Foo._instance=self.__A(*args,**kw) # 此处必须用Foo不能用self,受下面getattr方法的影响
    # 访问Foo的所有属性,都从_instance获取:也就是让Foo()对象拥有了内部类__A的所有方法和属性,可以直接使用外部类对象调用内部类的方法,如:f1.display(),而不需要:f1._instance.play()
    def __getattr__(self,attr):
        return getattr(Foo._instance,attr) # 此处必须用Foo不能用self,否则当变量_instance不存在时,hasattr(self,'_instance')会无限递归调用

测试

if __name__ == "__main__":
    f1=Foo('zhangsan',10) # 调用Foo.__init__()方法,由于此时类变量__instance==None,满足if条件,将_instance赋值为内部类__A的实例:如,<__main__.Foo.__A object at 0x00000283A28BC6A0>
    f2=Foo('yangqin',25) # 调用Foo.__init__()方法,类变量已经被赋值过,不为None,不再重新实例化__A。_instance还是最开始的对象。
    print(f1._instance is f2._instance)
    print(f1.display(),f2.display()) 
    print(f1.name,f2.name)
    print(f1.age,f2.age)

测试运行结果:可以看到多次实例化外部类Foo,调用的内部类__A都是相同的对象:都是第一次实例化的对象,从而实现了单例。

实现单例模式java 实现单例模式的内聚_实现单例模式java

过程分析

这种实现单例的过程可以总结为:先构造一个外部类,将真正起作用的内部类__A隐藏,并将__A的实例化对象赋值给外部类变量(Foo._instance),最后通过外部对象._instance就可以访问这个单例类了,为了进一步简化,定义了"__getattr__()"方法让外部类对象直接调用内部类的方法。

整个过程很类似于java中构造单例模式的方法:1.私有化构造方法;2.静态私有化实例变量并赋初值;3.提供实例对象的get公有方法)
注:下图java中的饿汉式单例

实现单例模式java 实现单例模式的内聚_类变量_02

弊端

python中并没有真正的私有属性,可以通过一定手段绕过绕过私有限制。并且上面的_instance也是可以直接访问的,调用者可以随意修改这个变量的值。此外,为了实现单例,构造一个外部类也太麻烦了
绕过方法:

if __name__ == "__main__":
    # 绕过方法
    f3=Foo('zhangsan',10)._Foo__A('zhangsan',10) #最后的属性值取决于后面的。
    f4=Foo('yangqin',25)._Foo__A('yangqin',25)
    print(f3.display(),f4.display())
    print(f3 is f4)
    print(f3.name,f4.name)
    print(f3.age,f4.age)

私有属性__A()可以通过“_类名__A()”绕过,从而导致单例对象被创建了多个不同的对象。

实现单例模式java 实现单例模式的内聚_类对象_03


此外,在多线程环境下,很容易出现问题,下面是启动30个线程打印的结果:可以看到创建的对象id不一样

实现单例模式java 实现单例模式的内聚_类对象_04

二、通过注解形式(限制Foo()调用形式)

主要思路是:既然python中不能真正将属性私有化,怎么限制‘Foo()’这种调用方式呢?因为调用Foo时,会调用其‘call()’方法,如果在这个方法中抛出异常,那么就不能直接调用了。而实现单例为了不用构造外部类,可以考虑pyhon中注解的语法。

class Singleton():
    def __init__(self,cls):
        self._cls=cls
    def instance(self,*args,**kw):
        try:
            return self._instance
        except AttributeError:
            self._instance=self._cls(*args,**kw)
            return self._instance

    def __call__(self,*args,**kw):
        raise TypeError('Singletons must be accessed through `Instance()`.')
    

@Singleton
class Foo():
    def __init__(self,name,age):
        self.name=name
        self.age=age

测试

if __name__ == "__main__":
    f1=Foo.instance('zhangsan',10) # <=> Singleton(Foo).instance('zhangsan',10)
    f2=Foo.instance('yangqin',25)
    print(f1 is f2)
    print(f1.name,f2.name)
    print(f1.age,f2.age)

实现单例模式java 实现单例模式的内聚_类变量_05

过程分析

采用注解时,f1=Foo.instance(‘zhangsan’,10)等价于 Singleton(Foo).instance(‘zhangsan’,10),第一次调用时由于没有_instance这个属性,会抛出AttributeError,但是程序捕获到该异常,并进行了处理:“self._instance=self._cls(*args,**kw)”,传进去的cls就是Foo,传进去的args为(‘zhangsan’,10).因此返回了一个Foo实例zhangsan,第二次调用时(f2=Foo.instance()),由于_instance已经有值了,直接返回第一次的Foo实例,从而实现了单例。此外,限制了直接通过Foo()实例化对象,当尝试“f1=Foo(‘zhangsan’,10)”这样调用时,实际上调用了Singleton(Foo)(‘zhangsan’,10)方法,也就是触发了Singleton的“__call__()”方法,抛出我们自定义的异常。

实现单例模式java 实现单例模式的内聚_实例化_06

弊端

看起来没有什么明显缺点,只是可能在注解方法中添加了一个额外的instance方法有点麻烦,如果不想通过instance这个变量来访问单例对象,而是直接用Foo()的方式实例化,那么可以考虑第三种方法

三、通过注解形式(保留Foo()调用形式)

class Singleton2():
    def __init__(self,cls):
        self._cls=cls
    def __call__(self,*args,**kw):
        # try:
        #     return self._instance
        # except AttributeError:
        #     self._instance=self._cls(*args,**kw)
        #     return self._instance
        # 下面这种写法也是相同效果
        if not hasattr(self,'_instance'):
            self._instance=self._cls(*args,**kw)
        return self._instance    
              
@Singleton2
class Foo():
    def __init__(self,name,age):
        self.name=name
        self.age=age

测试

if __name__ == "__main__":
    f1=Foo('zhangsan',10) # <=>Singleton(Foo)('zhangsan',10)
    f2=Foo('yangqin',25)
    print(f1 is f2)
    print(f1.name,f2.name)
    print(f1.age,f2.age)

实现单例模式java 实现单例模式的内聚_类变量_07

过程分析

当执行:f1=Foo(‘zhangsan’,10)时,等价于Singleton2(Foo)(‘zhangsan’,10),触发了Singleton2的call()方法,其赋值过程和上面第二种方式没有区别。多次调用也能得到同一个对象实例:始终为第一次实例化的对象。

四、通过重写__new__方法

以下说明帮助理解new方法的作用(引用自:):

__new__方法负责创建一个实例对象,在对象被创建的时候调用该方法它是一个类方法。__new__方法在返回一个实例之后,会自动的调用__init__方法,对实例进行初始化。如果__new__方法不返回值,或者返回的不是实例,那么它就不会自动的去调用__init__方法

class Foo():
    # self对象==__new__方法中返回的_instance
    def __init__(self,name,age):
        self.name=name
        self.age=age
    def __new__(cls,*args,**kw):
        if not hasattr(cls,'_instance'):
            cls._instance=super().__new__(cls)
        return cls._instance

测试

if __name__ == '__main__':
    f1=Foo('zhangsan',10) 
    f2=Foo('yangqin',25)
    print(f1 is f2)
    print(f1.name,f2.name)
    print(f1.age,f2.age)

注意,这里返回的是第二次的实例化结果

实现单例模式java 实现单例模式的内聚_类变量_08

过程分析

执行f1=Foo(‘zhangsan’,10) 首先调用Foo的new方法,发现没有类变量_instance,于是调用super().__new__(cls)方法创建一个cls对象,cls就是Foo对象,在调用init方法,int方法中的self就是new方法中返回的Foo对象_instance。然后赋值,name=zhangsan,age=10;
执行f2=Foo(‘yangqin’,25),还是先调用new方法,发现类变量_instance已经有值(name=zhangsan,age=10),所以直接返回,然后调用init方法,将init中self赋值,也就是说给类变量_instance赋新值,所以最终返回的结果看上去是第二次(准确的说应该是最后一次)的实例化结果。

五、元类实现单例

首先要明白元类的相关概念:类可以产生实例对象,那么什么产生类呢?元类产生类。假设有一个Person类和一个实例对象p,调用p()就调用了Person的call方法,那么类推一下:有一个元类Singleton,和一个实例化对象Person(这个Person实际上是我们经常叫的类,但是相对于元类来说,它就是一个实例化对象),当我们调用Person()去实例化Person的对象时,实际就调用了元类Singleton的call方法。python中还有一个内置的元类type,可以用它直接创建对象。metaclass属性就是来指定元类的,具体过程可以参考下面的引用说明:

class Foo(object,metaclass=Singleton):
    def __init__(self,name,age):
        self.name=name
        self.age=age

在该类并定义的时候,它还没有在内存中生成,直到它被调用。Python做了如下的操作:
1)Foo中有__metaclass__这个属性吗?如果是,Python会在内存中通过__metaclass__创建一个名字为Foo的类对象(我说的是类对象,请紧跟我的思路)。
2)如果Python没有找到__metaclass__,它会继续在父类中寻找__metaclass__属性,并尝试做和前面同样的操作。
3)如果Python在任何父类中都找不到__metaclass__,它就会在模块层次中去寻找__metaclass__,并尝试做同样的操作。
4)如果还是找不到__metaclass__,Python就会用内置的type来创建这个类对象。

使用元类实现单例代码:

class Singleton(type):
    def __init__(self, *args, **kwargs):
        self.__instance = None
        super().__init__(*args, **kwargs)

    def __call__(self, *args, **kwargs):
        if self.__instance is None:
            self.__instance = super().__call__(*args, **kwargs)
        return self.__instance
class Foo(object,metaclass=Singleton):
    def __init__(self,name,age):
        self.name=name
        self.age=age

测试

if __name__ == '__main__':
    f1=Foo('zhangsan',10) 
    f2=Foo('yangqin',25)
    print(f1 is f2)
    print(f1.name,f2.name)
    print(f1.age,f2.age)

实现单例模式java 实现单例模式的内聚_实现单例模式java_09

过程分析

注:这块逻辑我不是太确定,以下内容仅为根据断点调试执行顺序臆想,哈哈哈~

第一步:由于定义Foo类的时候通过metaclass指定了元类为Singleton,所以先调用元类的new和init方法返回Foo类对象,注意是Foo类对象本身而不是Foo类的实例对象

首先断点停到了Foo的init方法上,但是并没有进入里面

实现单例模式java 实现单例模式的内聚_类变量_10


下一步直接跳转到了元类的init方法,并且self是Foo类对象,前面知道init方法中的self就是new方法返回的,而通过下图看到self的值就是Foo类对象。所以这里猜想应该是通过metaclass属性指定元类后,先调用元类的new和init方法创建Foo类对象

实现单例模式java 实现单例模式的内聚_实现单例模式java_11


第二步:调用Foo(‘zhangsan’,10)方法,由于Foo可以看做元类Singleton的实例化对象,所以调用Foo()就会触发Singleton的call方法

实现单例模式java 实现单例模式的内聚_类对象_12


第一次调用时_instance是等于None的,触发了if逻辑,从而给Foo._instance赋值:super().call(*args, **kwargs)。

实现单例模式java 实现单例模式的内聚_类对象_13


第三步:调用super.call()方法,就是调用的Foo的__init__方法(至于原因暂时没有想明白,上图打印了super的值),从而给该Foo实例化对象赋值。赋值完成后将该instance返回。也就是f1的值:

实现单例模式java 实现单例模式的内聚_实例化_14


再次调用f2=Foo(‘yangqin’,25)时,同样先触发了元类的call方法,但是这次元类的instance是有值的(值就是f1),所以不会触发if逻辑,也就不调用super().call(*args, **kwargs),也就是不调用Foo的init方法,直接将f1的值返回。从而实现单例,并且是多次实例化对象始终返回的是第一个对象。

实现单例模式java 实现单例模式的内聚_类变量_15


最终输出结果:

实现单例模式java 实现单例模式的内聚_类对象_16

总结

以上五种实现单例的方法,只有第四种重写new方法返回的是最后实例化的对象,其他都是返回的第一次实例化的对象。最常用的还是通过注解和重写new方法来实现单例。要注意两者的区别。另外使用元类实现单例是比较少的,因为相较于其他形式,这个太难理解了!