Python 中 Singleton 的寫法及其擴充

浮生若夢的程式設計發表於2019-03-04

為何要有 Singleton ?

重要性無需多言,我們在專案中經常有「要一個程式全域性的變數(記憶體塊)」的需求,而且單例模式是幾種設計模式中最容易的。

偷懶且有用的做法:模組級別常量

我經常使用這種方式,因為簡單且不易出錯。

眾所周知,Python 的 module 概念,是一個天然的 Singleton。而且 Python 是多正規化語言,可以不必像 Java 那樣使用 class 去處理這件事情,在 module 級別定義一個常量,是一種很自然的想法。

程式碼如下:

  • 定義方
singleton.py

class _MySingleton(object):
    """
    我們使用下劃線開頭, 告誡呼叫者, 不要直接 new
    也不要來訪問這個class
    """
    def __init__(self, name, age):
        self._name = name
        self._age = age

    def print_name(self):
        print(self._name)

# 可以定製多個全域性例項
S1 = _MySingleton(`s1`, 22)
S2 = _MySingleton(`s2`, 11)
複製程式碼
  • 呼叫方
caller.py

from singleton import S1

# 盡情使用S1( 在任意點import 都可以 ), 它是全域性唯一的!
複製程式碼

正規做法:超程式設計

有些人不喜歡上面那種做法,他們認為「破壞了程式碼的純粹性」,這個時候我們可以使用超程式設計的方式,讓我們更近一步。

這是從 Python Cookbook 摘取出來的程式碼。

  • 元類基類
class SingletonMetaclass(type):
    def __init__(self, *args, **kwargs):
        self.__instance = None
        super().__init__(*args, **kwargs)

    def __call__(self, *args, **kwargs):
        if self.__instance is None:
            self.__instance = super(SingletonMetaclass, self).__call__(*args, **kwargs)
            return self.__instance
        else:
            return self.__instance
複製程式碼
  • 繼承了元類基類的類
class SpamSingleton(metaclass=SingletonMetaclass):
    """注意: 根據Singleton的定義, 建構函式一般需要使用預設的建構函式"""
    def get_addr(self):
        return id(self)
複製程式碼
  • 呼叫方
from singleton import SpamSingleton

s1 = SpamSingleton()
s2 = SpamSingleton()

pritn(id(s1), id(s2))   # 記憶體地址是一樣的
複製程式碼

老實說,理解 SingletonMetaclass 的作用過程還是有點困難的,我花了好久才搞清楚上面各個方法的呼叫流程。

不過 SingletonMetaclass 的作用也是巨大的,我們定義將其放入一個 base.py 的檔案中,任何時刻我們想要定義某個 Singleton class,直接從其繼承即可,簡單且方便。

比較 Hack 的做法:Borg 模式

不多說,搜尋引擎搜出來的結果,全都是推薦這種做法,讓人以為這是「主流做法」(其實並不是)。

這種做法,修改了 Singleton 的定義,即:所有變數共享一個記憶體塊,但是這個記憶體塊的內容是可變化的。然後通過將例項的 __dict__ 方法重定向 class 的 __dict__ 方法,以達到其目的。

但是這種做法不是很符合我對 Singleton的感知,即:全域性唯一,且其內容最好也不要變化。所以在實際開發中,並不喜歡這種做法。

更進一步

通常來說,更好的方法則是:在應用程式碼之外引入一個Manager,讓這個Manager來為我們建立和管理單例。這種做法也很普遍,就是我們通常所說的「依賴注入框架」。

如果使用 Spring,那麼一切都是很美妙的;如果沒有使用 Spring,我經常使用 Guice 來做我的依賴注入框架。

不過 Python 社群貌似對「依賴注入框架」、「IoC容器」等等都不怎麼感冒(其實是好東西),在此先不提。

總結

從個人的傾向來看,比較喜歡「掌握一種或兩種方式,然後使用最熟練,而且不出錯」的觀點,所以我只推薦第一二種方式。

相關文章