# Python單例模式最佳實踐

前言

今天在羣裏討論時討論到了單例模式,這應該是你們最熟悉的一種設計模式了。python

簡單而言,單例模式就是保證某個實例在項目的整個生命週期中只存在一個,在項目的任意位置使用,都是同一個實例。編程

單例模式雖然簡單,但仍是有些門道的,而少有人知道這些門道。bootstrap

邊界狀況

Python中實現單例模式的方法不少,我之前最常使用的應該是下面這種寫法。設計模式

class Singleton(object):
    
    _instance = None
    def __new__(cls, *args, **kw):
        if cls._instance is None:
            cls._instance = object.__new__(cls, *args, **kw)
        return cls._instance
複製代碼

這種寫法有兩個問題。多線程

1.單例模式對應類實例化時沒法傳入參數,將上面的代碼擴展成下面形式。app

class Singleton(object):
    
    _instance = None
    def __new__(cls, *args, **kw):
        if cls._instance is None:
            cls._instance = object.__new__(cls, *args, **kw)
        return cls._instance

    def __init(self, x, y):
        self.x = x
        self.y = y

s = Singleton(1,2)
複製代碼

此時會拋出TypeError: object.__new__() takes exactly one argument (the type to instantiate)錯誤ide

2.多個線程實例化Singleton類時,可能會出現建立多個實例的狀況,由於頗有可能多個線程同時判斷cls._instance is None,從而進入初始化實例的代碼中。spa

基於同步鎖實現單例

先考慮上述實現遇到的第二個問題。線程

既然多線程狀況下會出現邊界狀況從而參數多個實例,那麼使用同步鎖解決多線程的衝突則可。設計

import threading

# 同步鎖
def synchronous_lock(func):
    def wrapper(*args, **kwargs):
        with threading.Lock():
            return func(*args, **kwargs)
    return wrapper

class Singleton(object):
    instance = None

 @synchronous_lock
    def __new__(cls, *args, **kwargs):
        if cls.instance is None:
            cls.instance = object.__new__(cls, *args, **kwargs)
        return cls.instance
複製代碼

上述代碼中經過threading.Lock()將單例化方法同步化,這樣在面對多個線程時也不會出現建立多個實例的狀況,能夠簡單試驗一下。

def worker():
    s = Singleton()
    print(id(s))

def test():
    task = []
    for i in range(10):
        t = threading.Thread(target=worker)
        task.append(t)
    for i in task:
        i.start()
    for i in task:
        i.join()

test()
複製代碼

運行後,打印的單例的id都是相同的。

更優的方法

加了同步鎖以後,除了沒法傳入參數外,已經沒有什麼大問題了,可是否有更優的解決方法呢?單例模式是否有能夠接受參數的實現方式?

閱讀Python官方的wiki(wiki.python.org/moin/Python…

def singleton(cls):
    cls.__new_original__ = cls.__new__

 @functools.wraps(cls.__new__)
    def singleton_new(cls, *args, **kwargs):
        it = cls.__dict__.get('__it__')
        if it is not None:
            return it
        
        cls.__it__ = it = cls.__new_original__(cls, *args, **kwargs)
        it.__init_original__(*args, **kwargs)
        return it

    cls.__new__ = singleton_new
    cls.__init_original__ = cls.__init__
    cls.__init__ = object.__init__
    return cls
    
@singleton
class Foo(object):
    def __new__(cls, *args, **kwargs):
        cls.x = 10
        return object.__new__(cls)

    def __init__(self, x, y):
        assert self.x == 10
        self.x = x
        self.y = y

複製代碼

上述代碼中定義了singleton類裝飾器,裝飾器在預編譯時就會執行,利用這個特性,singleton類裝飾器中替換了類本來的__new____init__方法,使用singleton_new方法進行類的實例化,在singleton_new方法中,先判斷類的屬性中是否存在__it__屬性,以此來判斷是否要建立新的實例,若是要建立,則調用類本來的__new__方法完成實例化並調用本來的__init__方法將參數傳遞給當前類,從而完成單例模式的目的。

這種方法讓單例類能夠接受對應的參數但面對多線程同時實例化仍是可能會出現多個實例,此時加上線程同步鎖則可。

def singleton(cls):
    cls.__new_original__ = cls.__new__
 @functools.wraps(cls.__new__)
    def singleton_new(cls, *args, **kwargs):
        # 同步鎖
        with threading.Lock():
            it = cls.__dict__.get('__it__')
            if it is not None:
                return it
            
            cls.__it__ = it = cls.__new_original__(cls, *args, **kwargs)
            it.__init_original__(*args, **kwargs)
            return it

    cls.__new__ = singleton_new
    cls.__init_original__ = cls.__init__
    cls.__init__ = object.__init__
    return cls
複製代碼

是否加同步鎖的額外考慮

若是一個項目不須要使用線程相關機制,只是在單例化這裏使用了線程鎖,這其實不是必要的,它會拖慢項目的運行速度。

閱讀CPython線程模塊相關的源碼,你會發現,Python一開始時並無初始化線程相關的環境,只有當你使用theading庫相關功能時,纔會調用PyEval_InitThreads方法初始化多線程相關的環境,代碼片斷以下(我省略了不少不相關代碼)。

static PyObject * thread_PyThread_start_new_thread(PyObject *self, PyObject *fargs) {
    PyObject *func, *args, *keyw = NULL;
    struct bootstate *boot;
    unsigned long ident;
    
    // 初始化多線程環境,解釋器默認不初始化,只有用戶使用時,才初始化。
    PyEval_InitThreads(); /* Start the interpreter's thread-awareness */
    // 建立線程
    ident = PyThread_start_new_thread(t_bootstrap, (void*) boot);
    
    // 返回線程id
    return PyLong_FromUnsignedLong(ident);
}
複製代碼

爲何會這樣?

由於多線程環境會啓動GIL鎖相關的邏輯,這會影響Python程序運行速度。不少簡單的Python程序並不須要使用多線程,此時不須要初始化線程相關的環境,Python程序在沒有GIL鎖的狀況下會運行的更快。

若是你的項目中不會涉及多線程操做,那麼就沒有使用有同步鎖來實現單例模式。​

結尾

互聯網中有不少Python實現單例模式的文章,你只須要從多線程下是否能夠保證單實例以及單例化時是否能夠傳入初始參數兩點來判斷相應的實現方法則可。

歡迎關注「懶編程」,一塊兒探索技術的本質。

相關文章
相關標籤/搜索