在之前的博客中,我们提到了logging模块中的rootlogger是单例模式,那么我们就来探讨下单例模式在python中的实现。什么是单例模式(singleton)单例模式是一种创建对象行为的设计模式,它要求一个类只能创建和存储一个实例化的对象。为什么要使用单例模式?单例模式是为了避免不必要的内存浪费,比如多个线程调用一个类,但实际上没有必要为每个线程都提供一个类的实例;也是为了防止资源被多次占用,比如多个线程要对同一个文件对象进行操作,创建多个句柄是不合理的。单例模式的设计逻辑那么如何设计单例模式呢?通常,有两个重要的设计逻辑:饥饿模式:在加载类时或至少在使用类之前创建一个单例实例。即无论后面是否使用该类,都会创建一个实例。懒惰模式:第一次需要使用一个类时创建一个单例实例。对于java和c++,都有相应的饿汉模式和懒汉模式的实现。下面是它们的简单实现(不考虑线程安全)。java//java饥饿模式publicclassSingleton{//Instance在类中直接定义为Singleton实例,//即类加载时创建单例privatestaticSingletoninstance=newSingleton();//hiddenConstructorprivateSingleton(){}//暴露唯一可以获取类对象实例的接口getInstancestaticfunctionpublicstaticSingletongetInstance(){returninstance;}}//java惰性模式publicclassSingleton{//隐藏构造函数privateSingleton(){}//初始化实例为nullprivatestaticSingletoninstance=null;//暴露唯一可以获取类对象实例的接口getInstancestaticfunctionpublicstaticSingletongetInstance(){//第一次调用时,instance为Null,所以通过if判断if(instance==null){//获取单例实例=newSingleton();}//调用该接口后,只会返回当前存在的单例返回实例;}}C++//c++饿汉模式classSingleon{private://隐藏构造函数接口Singleon(){};//C++不能在类定义中赋值,const静态修饰方法只能用于整数//定义一个静态类成员,不属于任何实例staticSingleon*instance;public:staticSingleon*GetSingleon(){返回实例;}staticSingleon*Destroy(){删除实例;实例=空;}};//这里要注意,对于c++类中的静态数据成员,无论其访问限制是private还是public,其初始化都是如下形式//(类型名)(类名)::(staticdatamembername)=(value);//这里newSingleton(),即使我们私有化了构造函数//访问限制,在初始化静态数据成员的时候//仍然可以直接使用,然后再使用是不合法的private//其他任何地方的构造函数接口Singleon*Singleon::instance=newSingleton();//c++惰性模式classSingleon{private://也隐藏了构造函数接口Singleon(){};静态单身*实例;public://暴露接口staticSingleon*GetSingleon(){//判断静态数据成员实例的值if(instance==NULL){instance=newSingleon();}返回实例;}staticSingleon*Destroy(){删除实例;实例=空;}};Singleon*Singleon::instance=NULL;可以看出java是直接在类中静态成员可以在类内部初始化,所以在加载的时候按照饿了么模式的设计可以直接生成一个类的单个实例,而C++是不能初始化静态数据的类内的成员作为类实例,所以需要在类定义之外对其进行初始化,形成单例设计模式至于python,和c++类似,不能在类中将属性初始化为类的实例,如:classTest:t=Test().....这段代码是错误的,因为对于python在加载时class,类中的语句被认为是可执行的,执行t=Test()时,还没有定义Test类,所以会出现矛盾错误。python类中命名空间和作用域的解析在python中,命名空间和作用域的概念其实是一个和两个方面。命名空间着重于名称与对象的映射关系,保证名称之间不发生冲突。python中存在三种命名空间:localnamespace(函数内,类内)globalnamespacebuiltinnamespace和scope描述了查找变量的问题(直接访问),为此定义了一个层次结构:LEGB(Local->Enclosing->全局->内置)。类中的命名空间在python中是很正常的,属于局部命名空间,但是对于类中的作用域来说,在LEGB的层次结构中是一种特殊的存在。我们用一段代码来解释一下:var=123str="loveyou!"测试类:var=19print(str)deffun(self):print(var)t=Test()t.fun()--------------------------上面代码执行后输出如下:loveyou!123可见类的方法直接忽略了类中的作用域(当然受限于类名:Test.var也可以访问,但是我们现在讨论的LEGB是直接访问的问题),直接找global中的变量var,类中的print(str)也可以找globalstr。所以简单来说:1.python类中的作用域对于类中的方法是不可见的,也就是说类中定义的变量或者其他方法不能被某个方法直接引用。2.看python类内外是否符合LEGB规则。Python单例模式设计这里只简单介绍使用__new__方法进行单例设计的方式,以后有时间再补充。首先,python不能像java和c++那样隐藏接口,也不能在类定义中将class属性初始化为类实例。在这个前提下,我们使用__new__方法来讨论单例模式的设计(new其实相当于半个构造函数,只构造一个实例,没有初始化):#所谓的饿鬼模式classSingleton:def__new__(cls):ifnothasattr(Singleton,"instance"):cls.instance=super().__new__(cls)returncls.instance#InstantiateSingleton()beforeuse#Lazymanmode->可以说是so-叫饿汉模式是在懒汉模式后添加了Singleton()类Singleton:def__new__(cls):ifnothasattr(Singleton,"instance"):cls.instance=super().__new__(cls)returncls.instance看完上面的代码,也许你会觉得有点牵强,而且我觉得python的饿鬼模式有点“假”,但是饿鬼懒人其实是一种设计逻辑,只要相应的逻辑就完成了,具体代码的区别不重要!线程安全的python单例模式这里需要进一步介绍python单例模式的线程安全。由于我们上面介绍的__new__方法与单例模式的饿懒代码区别不大,所以这里使用懒人模式,线程安全的介绍见代码:importtimeimportthreadingclassSingleton:def__new__(cls):#假设两个线程对这个if判断都执行一次,#thead1通过,然后继续执行time.sleep(1),#那么在这1秒sleep中,thread2也通过了#if判断,那么显然如果没有hasattr(Singleton,"instance"):time.sleep(1)cls.instance=super().__new__(cls)returncls.instance,稍后将创建两个实例针对以上情况,我们使用线程锁来改进类Singleton:lock=threading.lockdef__new__(cls):#还是一样的情况,执行两个thead到这个判断,#thead1先通过,执行nextsentencewiththreading.Lock()#然后锁直接被占用,然后在thread1的1秒sleep中#thread2也通过了第一个if判断,继续执行#withthreading.Lock()语句,无法抢占锁并阻塞#Whenthread1完成1秒的sleep,并传递第二个if,#给cls.instance赋值,withcontext退出后,thread2#可以继续执行,然后进行第二个ifsleep1秒后判断,#此时不能通过是的,因为thread1已经创建了一个实例#然后我们要带着context退出,然后执行returncls.instance#其实就是返回创建的cls.instancethread1ifnothasattr(Singleton,"instance"):#threading.Lock()支持contextmanagerprotocolwithSingleton.Lock():time.sleep(1)ifnothasattr(Singleton,"instance"):cls.instance=super().__new__(cls)returncls.instanceabovesingletonpattern线程安全的实现方式称为:双重检测机制两个if判断,第一个if判断是为了防止每个线程抢占锁,然后再去执行相应的代码,会浪费时间;第二个if判断比较关键,if多个线程通过第一个if判断进入锁的抢占。如果没有第二个if判断,依然会为每个线程生成一个实例,无法完成单例模式。
