python設計模式-單例模式

Rnan-prince發表於2020-11-25

問題:現代化的巧克力工廠具備計算機控制的巧克力鍋爐。鍋爐做的事情就是把巧克力和牛奶融在一起,然後送到下一個階段,以製成巧克力棒。下邊是一個巧克力公司鍋爐控制器的程式碼,仔細觀察一下,這段程式碼有什麼問題?

class ChocolateBoiler(object):

    def __init__(self):
        self.empty = True
        self.boiled = False

    def fill(self):
        # 向鍋爐填充巧克力和牛奶混合物
        # 在鍋爐內填充原料時,鍋爐必須是空的。
        # 一旦填入原料,就要把empty 和 boiled 標誌設定好
        if self.empty:
            self.empty = False
            self.boiled = False

    def drain(self):
        # 排出煮沸的巧克力和牛奶
        # 鍋爐排出時,必須是滿的且煮沸的。
        # 排出完畢empty 設定為 true
        if not self.empty and self.boiled:
            self.empty = True

    def boil(self):
        # 將顱內物煮沸
        # 煮混合物時,鍋爐內必須是滿的且沒有煮沸過
        # 一旦煮沸,就把 boiled 設定為 true
        if not self.empty and not self.boiled:
            self.boiled = True複製程式碼

從程式碼可以看出,他們加入了多種判斷,以防止不好的事情發生。如果同時存在兩個ChocolateBoiler例項,那這麼多判斷豈不是失去作用了。那我們改如何實現這個需求呢?這個問題的核心是,我們要先判斷例項是不是已經存在,如果存在就不再建立。

_chocolate_boiler_instance = None  # 宣告例項

def chocolate_boiler():
    global _chocolate_boiler_instance  # 使用全域性變數

    if _chocolate_boiler_instance is not None: # 判斷是否存在,如果存在,直接返回
        return _chocolate_boiler_instance
    else:
        # 如果不存在,建立一個新的
        _chocolate_boiler_instance = ChocolateBoiler()
        return _chocolate_boiler_instance複製程式碼

現在我們需要獲取 ChocolateBoiler 例項的時候只需要呼叫 chocolate_boiler 方法獲取例項即可保證同時只有一個 ChocolateBoiler例項。

這種保證 ChocolateBoiler類只有一個例項,並提供一個全域性訪問點的模式,就是單例模式

單例模式

定義

單例模式:確保一個類只有一個例項,並提供一個全域性訪問點。

  • 也就是說,我們使用單例模式要把某個類設計成自己管理的一個單獨例項,同時也避免其他類再自行產生例項。並且只允許通過單例類獲取單例的例項。
  • 我們也提供對這個例項的全域性訪問點:當你需要例項時,像類查詢,它會返回單個例項。

實現

python 實現單例模式有多種方案:

使用 metaclass

《python cookbook》提供了非常易用的 Singleton 類,只要繼承它,就會成為單例。

# python 3 程式碼實現
class Singleton(type):

    def __init__(self, *args, **kwargs):
        self.__instance = None
        super().__init__(*args, **kwargs)

    def __call__(self, *args, **kwargs):
        if self.__instance is None:
            # 如果 __instance 不存在,建立新的例項
            self.__instance = super().__call__(*args, **kwargs)
            return self.__instance
        else:
            # 如果存在,直接返回
            return self.__instance


class Spam(metaclass=Singleton):

    def __init__(self):
        print('Creating Spam')

a = Spam()
b = Spam()

print(a is b)  # 這裡輸出為 True複製程式碼

元類(metaclass)可以控制類的建立過程,它主要做三件事:

  • 攔截類的建立
  • 修改類的定義
  • 返回修改後的類

例子中我們構造了一個Singleton元類,並使用call方法使其能夠模擬函式的行為。構造類 Spam 時,將其元類設為Singleton,那麼建立類物件 Spam 時,行為發生如下:

Spam = Singleton(name,bases,class_dict),Spam 其實為Singleton類的一個例項。

建立 Spam 的例項時,Spam()=Singleton(name,bases,class_dict)()=Singleton(name,bases,class_dict).call(),這樣就將 Spam 的所有例項都指向了 Spam 的屬性 __instance上。

使用 new

我們可以使用 new 來控制例項的建立過程,程式碼如下:

class Singleton(object):

    __instance = None

    def __new__(cls, *args, **kw):
        if not cls.__instance:
            cls.__instance = super().__new__(cls, *args, **kw)
        return cls.__instance

class Foo(Singleton):
    a = 1

one = Foo()
two = Foo()
assert one == two
assert one is two
assert id(one) == id(two)複製程式碼

通過 new 方法,將類的例項在建立的時候繫結到類屬性 instance 上。如果cls.instance 為None,說明類還未例項化,例項化並將例項繫結到cls.instance 以後每次例項化的時候都返回第一次例項化建立的例項。注意從Singleton派生子類的時候,不要過載new__。

使用裝飾器

import functools

def singleton(cls):
    ''' Use class as singleton. '''
    # 首先將 __new__ 方法賦值給 __new_original__
    cls.__new_original__ = cls.__new__

    @functools.wraps(cls.__new__)
    def singleton_new(cls, *args, **kw):
        # 嘗試從 __dict__ 取 __it__
        it =  cls.__dict__.get('__it__')
        if it is not None: # 如果有值,說明例項已經建立,返回例項
            return it
        # 如果例項不存在,使用 __new_original__ 建立例項,並將例項賦值給 __it__
        cls.__it__ = it = cls.__new_original__(cls, *args, **kw)
        it.__init_original__(*args, **kw)
        return it
    # class 將原有__new__ 方法用 singleton_new 替換
    cls.__new__ = singleton_new
    cls.__init_original__ = cls.__init__
    cls.__init__ = object.__init__

    return cls

#
# 使用示例
#
@singleton
class Foo:
    def __new__(cls):
        cls.x = 10
        return object.__new__(cls)

    def __init__(self):
        assert self.x == 10
        self.x = 15


assert Foo().x == 15
Foo().x = 20
assert Foo().x == 20複製程式碼

這種方法的內部實現和使用 __new__ 類似:

  • 首先,將 new 方法賦值給 new_original,原有 new 方法用 singleton_new 替換,定義 init_original 並將 cls.init 賦值給 init_original
  • 在 singleton_new 方法內部,嘗試從 dictit(例項)
  • 如果例項不存在,使用 new_original 建立例項,並將例項賦值給 it,然後返回例項

最簡單的方式

將名字singleton繫結到例項上,singleton就是它自己類的唯一物件了。

class singleton(object):
    pass
singleton = singleton()複製程式碼

github.com/gusibi/Meti… 使用的就是這種方式,用來獲取全域性的 request

Python 的模組就是天然的單例模式,因為模組在第一次匯入時,會生成 .pyc 檔案,當第二次匯入時,就會直接載入 .pyc 檔案,而不會再次執行模組程式碼。因此,我們只需把相關的函式和資料定義在一個模組中,就可以獲得一個單例物件了。

參考連結


最後,感謝女朋友支援。

歡迎關注(April_Louisa) 請我喝芬達
歡迎關注
歡迎關注
請我喝芬達
請我喝芬達

相關文章