1

在前一篇blog中我们提到了logging模块中的root logger是单例模式的,所以紧接着我们就来探索一下单例模式在python中的实现。

什么是单例模式(singleton)

单例模式是一种对于创建对象行为的设计模式,该模式要求一个类只能创建并存有一个实例化对象。
而为什么使用单例模式呢?
单例模式是为了避免不必要的内存浪费,如多个线程调用一个类,但实际并不需要对每个线程都提供一个该类的实例;同样也是为了防止对资源的多重占用,如多个线程要操作同一个文件对象,那么创建多个句柄是不合理的。

单例模式的设计逻辑

那么如何设计一个单例模式呢?一般的有两个重要的设计逻辑:

  1. 饿汉模式:在类加载时或者至少在类被使用前创建一个单例实例。即不管后面有没有使用该类都创建一个实例。
  2. 懒汉模式:在第一次需要使用类的时候创建一个单例实例。

对于java与c++都有对应的饿汉模式与懒汉模式的实现,这里介绍一下它们的简单实现(不考虑线程安全的情况)。

Java

//java饿汉模式
public class Singleton {
    //直接在类中定义了instance作为Singleton实例,
    //即在类加载时就创建出一个单例
    private static Singleton instance = new Singleton();
    //隐藏构造函数
    private Singleton(){}
    //暴露唯一可以得到类对象实例的接口getInstance静态函数
    public static Singleton getInstance(){
        return instance;
    }
}

//java懒汉模式
public class Singleton {
    //隐藏构造函数
    private Singleton() {}  
    //把instance初始化为null
    private static Singleton instance = null;  
    //暴露唯一可以得到类对象实例的接口getInstance静态函数
    public static Singleton getInstance() {
        //第一次被调用时,instance是null,所以if判断通过
        if (instance == null) {
            //得到单例
            instance = new Singleton();
        }
        //之后再调用该接口只会返回当前存在的单例
        return instance;
    }
}

C++

//c++饿汉模式
class Singleon
{
private:
    //隐藏构造函数接口
    Singleon(){};
    //C++中无法在类定义中赋值,const static的修饰方法也只能用于整型
    //定义一个静态类成员,不属于任何一个实例
    static Singleon* instance;
public:
     
    static Singleon* GetSingleon()
    {
        return instance;
    }
    static Singleon* Destroy()
    {
        delete instance;
        instance = NULL;
    }
};
//注意这里,对于c++类中的静态数据成员,不论它访问限制
//是private还是public的,它的初始化都是以如下的形式
//(类型名)(类名)::(静态数据成员名)=(value);
//而且这里的new Singleton(),即使我们已经把构造函数
//的访问限制私有化了,在对静态数据成员初始化的时候
//还是可以直接使用,之后在其它任何地方使用私有化的
//构造函数接口都是非法的
Singleon* Singleon::instance = new Singleton() ;

//c++懒汉模式
class Singleon
{
private:
    //同样隐藏构造函数接口
    Singleon(){};
    static Singleon*instance;
public:
    //暴露的接口
    static Singleon* GetSingleon()
    {
        //判断静态数据成员instance的值
        if (instance==NULL)
        {
            instance = new Singleon();
        }
        return instance;
    }
    static Singleon* Destroy()
    {
        delete instance;
        instance = NULL;
    }
};
Singleon* Singleon::instance =  NULL;

可以看出java直接在类内就可以初始化静态成员,所以在加载的时候就可以按照饿汉模式的设计直接生成一个类的单例,而c++不能在类内初始化静态数据成员为类实例,所以还需要在类定义外对其初始化,来形成单例设计模式。至于python,和c++类似,它也不能在类内初始化属性为类的实例,如:

class Test:
    t=Test()
    .....

这段代码是错误的,因为对于python在加载类的时候,把类内语句作为可执行的,而执行t=Test()时,Test类还未定义出来,因此矛盾出错。

python类内namespace和scope的解析

在python中namespace和scope其实一体两面的概念,namespace着眼于name与object之间的映射关系,确保name之间不存在conflict,python中存在三种namespace:

  1. local namespace(函数内,类内)
  2. global namespace
  3. builtin namespace

而scope则描述的是变量(directly access)寻找的问题,它为此定义了一个hierarchy:LEGB(Local->Enclosing->Global->Builtin)。

对于python中类内的namespace是正常的,属于local namespace,但是对于类内的scope,它是LEGB这个hierarchy中的一个特殊存在。下面我们借由一段代码解释一下:

var=123
str="love you!"

class Test:
    var=19
    print(str)
    def fun(self):
        print(var)

t=Test()
t.fun()
--------------------------
上面这段代码执行后,输出如下:
love you!
123

可见,类的method直接无视了类内的scope(当然用类名限定:Test.var也是可以访问到的,但是我们现在讨论的LEGB是directly access的问题),直接找到的是global中的变量var,而类内的print(str)也可以找到global的str。那么简单来说:1.python类内scope对类内method不可见,也就是说类内定义的变量或是其他method都不能被某一个method直接引用。2.python类内向外寻找是符合LEGB规则的。

python单例模式设计

我们这里只简单介绍一下使用__new__方法进行单例设计的方式,以后若有时间再进行补充。
首先python没法像java和c++一样把接口隐藏起来,同时也无法在类定义中初始化类属性为类实例。以此为前提,我们使用__new__方法来讨论单例模式的设计(new其实相当于半个构造函数,只构造实例不初始化):

#所谓饿汉模式
class Singleton:
    def __new__(cls):
        if not hasattr(Singleton,"instance"):
            cls.instance=super().__new__(cls)
        return cls.instance
#在使用前实例化
Singleton()

#懒汉模式->可以说所谓的饿汉模式就是在懒汉模式后加了Singleton()
class Singleton:
    def __new__(cls):
        if not hasattr(Singleton,"instance"):
            cls.instance=super().__new__(cls)
        return cls.instance

看完以上的代码也许你会觉得有些牵强,觉得python的饿汉模式有些“假”,但是饿汉懒汉其实都是一种设计逻辑罢了,只要完成了相应的逻辑,具体的代码之间的区别并不重要!

线程安全的python单例模式

这里需要对python单例模式的线程安全进行进一步的介绍,由于上面我们介绍的__new__方法设计单例模式的饿汉懒汉代码差别不大,我们这里就以懒汉模式进行线程安全的介绍,看代码:

import time
import threading

class Singleton:
    def __new__(cls):
        #假设有两个thread都一次执行到了这个if判断,
        #thead1通过,然后继续执行time.sleep(1),
        #那么在这个1秒的sleep中,thread2也通过了
        #这个if判断,则后面很显然的会创建两个实例
        if not hasattr(Singleton,"instance"):
            time.sleep(1)
            cls.instance=super().__new__(cls)
        return cls.instance
        
针对以上情况,我们使用线程锁进行改进
class Singleton:
    lock=threading.lock
    def __new__(cls):
        #还是相同的情况,两个thead执行到这个判断,
        #thead1先通过,执行下一句with threading.Lock()
        #那么便直接占有了锁,之后在thread1的1秒sleep中
        #thread2也通过了第一个if判断,而继续执行执行
        #with threading.Lock()语句,无法抢占锁,被阻塞
        #当thread1完成1秒的sleep后,并且通过第二个if,
        #对cls.instance赋值,退出with context后,thread2
        #才能继续执行,1秒sleep之后再进行第二个if判断,
        #此时不能通过了,因为thread1已经创建了一个instance
        #那么只好退出with context,再执行return cls.instance
        #其实就是返回thread1创建的cls.instance
        if not hasattr(Singleton,"instance"):
            #threading.Lock()支持context manager protocol
            with Singleton.Lock():
                 time.sleep(1)
                 if not hasattr(Singleton,"instance"):
                        cls.instance=super().__new__(cls)
            return cls.instance
            

以上的单例模式线程安全实现方法叫做:双重检测机制。两个if判断,其中第一个if判断是为了防止每个thread都要进行抢占锁然后执行对应代码,会很浪费时间;而第二个if判断则更加关键,如果有多个thread都通过了第一个if判断,进入锁的抢占,如果没有第二个if判断,那么还是会每个thread生成一个实例,无法完成单例模式。


鼠与我
7 声望1 粉丝

Sad