Python單例模式初探

在前一篇blog中咱們提到了logging模塊中的root logger是單例模式的,因此緊接着咱們就來探索一下單例模式在python中的實現。java

什麼是單例模式(singleton)

單例模式是一種對於建立對象行爲的設計模式,該模式要求一個類只能建立並存有一個實例化對象。
而爲何使用單例模式呢?
單例模式是爲了不沒必要要的內存浪費,如多個線程調用一個類,但實際並不須要對每一個線程都提供一個該類的實例;一樣也是爲了防止對資源的多重佔用,如多個線程要操做同一個文件對象,那麼建立多個句柄是不合理的。python

單例模式的設計邏輯

那麼如何設計一個單例模式呢?通常的有兩個重要的設計邏輯:c++

  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++相似,它也不能在類內初始化屬性爲類的實例,如:ui

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

這段代碼是錯誤的,由於對於python在加載類的時候,把類內語句做爲可執行的,而執行t=Test()時,Test類還未定義出來,所以矛盾出錯。spa

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生成一個實例,沒法完成單例模式。

相關文章
相關標籤/搜索