用Python實現設計模式——單例模式

夏秋發表於2019-02-16

前言

單例模式是設計模式(Design Pattern)中最簡單、最容易理解的一種,維基百科[1]的定義如下:

單例模式,也叫單子模式,是一種常用的軟體設計模式。在應用這個模式時,單例物件的 “類 (電腦科學)”)必須保證只有一個例項存在。許多時候整個系統只需要擁有一個的全域性物件,這樣有利於我們協調系統整體的行為。

單例模式的主要優點是共享資源和減少資源消耗,主要應用於IO或資料庫的執行緒池,快取,日誌,對話和需共享資料的資源等,但是在實現情況中濫用單例模式會帶來很多意想不到的問題,本文重點在於介紹幾種Python實現單例模式的方法,這裡就不再展開論述了。文中所演示的程式碼都會託管在Github上。

簡單實現

首先,我們先嚐試用Python內部類(巢狀類)來實現單例模式:

#coding=utf-8
class Singleton:
    """單列類
    """
    class __MyClass:
        """實際生成例項的類
        """
        def __init__(self, arg):
            """初始化並賦值"""
            self.foo = arg

        def display(self):
            """返回例項的id和屬性值"""
            return (id(self), self.foo)

    # 類屬性
    _instance = None
    def __init__(self, arg):
        if not Singleton._instance:
            Singleton._instance = Singleton.__MyClass(arg)
        else:
            Singleton._instance.foo = arg

    def __getattr__(self, attr):
        return getattr(self._instance, attr)

注意實際生成例項的類是內部的“__MyClass”類,前面的雙下劃線代表這是一個私有的類,使用者不能再外面直接訪問它。而在“__MyClass”類外封裝了一個“Singleton”類,這個類的任務就是在初始化時保證整個上下文中只有一個例項,實現的方式很簡單。用一個私有屬性__instance_儲存當前生成的例項,在初始化時判斷例項是否為_None_,如果是就用“__MyClass”類生成一個新例項並賦值給__instance_,否就直接返回或呼叫當前__instance_的例項。最後用“__MyClass”裡的實現的方法測試一下:

if __name__ == "__main__":
    """測試"""
    s1 = Singleton("bar")
    s2 = Singleton("zoo")
    print(s1.display())
    print(s2.display())

# output
>(41706760L, `zoo`)
>(41706760L, `zoo`)

基類

現在我們考慮將inner class拆分出來,因為在Python類例項化時會呼叫___new___方法[2]來生成例項,所以我們可以先繼承“Singleton”類,然後通過重寫基類的___new___方法讓其實現單例模式:

#coding=utf-8
class Singleton(object):
    """單例類
    """
    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
        return cls._instance

class MyClass(Singleton):
    """實際生成例項的類
    """
    def __init__(self, arg):
        self.foo = arg

    def display(self):
        return (id(self), self.foo)

測試結果:

if __name__ == "__main__":
    s1 = MyClass("bar")
    s2 = MyClass("zoo")
    print(s1.display())
    print(s2.display())
    assert s1 is s2

# output
>(40882416L, `zoo`)
>(40882416L, `zoo`)

裝飾器

第三種就是最常見的用裝飾器來實現單列模式:

#coding=utf-8
def singleton(cls):
    instances = {}
    def wrapper(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return wrapper

@singleton
class MyClass:
    """實際生成例項的類
    """
    foo = "foo"
    def display(self):
        return (id(self))

@singleton
class OtherClass:
    """另一個類
    """
    pass

裝飾器的實現過程是將生成的例項都放到一個名為_instances_的Dict中對映好,這樣每次在類初始化時先檢查_instances_中是否已經包含有例化好的例項,有就直接返回是咧,沒有則呼叫類初始化一個並賦值給_instances_列表。裝飾器的好處在於用一個Dict列表來管理所有需要實現單例模式的類,更簡便和通用化。程式碼的測試結果如下:

if __name__ == "__main__":
    s1 = MyClass()
    s1.foo = "bar"
    print(s1.display(), s1.foo)
    s2 = MyClass()
    s2.foo = "zoo"
    print(s2.display(), s2.foo)
    assert s1 is s2
    s3 = OtherClass()
    s4 = OtherClass()
    assert s3 is s4

元類

如果希望不僅僅是通過限制而是在源頭上就建立一個單例類,我們需要用到元類來實現,元類可以參考Stackoverflow[3]上的一個解答。簡單的說就是Python中的類也是一種物件,被稱為類物件。類物件可以通過元類type來建立,而在此過程中會呼叫type__call__ 方法。所以我們只要在type建立類物件的過程中重寫 __call__ 方法,在其中加入相應的建立單例的邏輯即可實現單例模式,具體程式碼實現如下:

#coding=utf-8
class Singleton(type):
    def __call__(cls, *args, **kwargs):
        """重寫,實現單例模式"""
        if not hasattr(cls, `_instance`):
            cls._instance = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instance

class MyClass(object):
    # 指定元類
    __metaclass__ = Singleton

    def display(self):
        return (id(self))

程式碼的測試與前面類似,這裡就不再累述了。

執行緒安全

最後,需要注意的是單例模式在多執行緒下可能會出現執行緒安全的問題,這時候就需要在單例的初始化過程中加上執行緒同步鎖來避免,但這樣又會降低整體的效能,具體可以參考這篇文件

參考

[1]維基百科
[2]Python官方文件
[3]Stackoverflow

相關文章