HOWTO · Python
Python 中的單例設計模式
本教程演示了單例設計模式在 Python 中的實現。
本頁內容
Singleton 會將某個類別限制為在指定範圍內共用同一個實例。在 Python 中,可以透過裝飾器、基底類別、元類或模組狀態來模擬這種行為。每種做法都會改變物件的建立方式,而且都不會自動提供執行緒安全,也不會讓不同程序共用同一個物件。以下範例會檢查物件識別,協助你依照程式使用類別的方式選擇做法。
Singleton 在 Python 中的運作方式
一般情況下,每次呼叫類別都會建立一個新物件。Singleton 會改變這個建立流程,讓重複呼叫時回傳同一個物件。這項保證通常只適用於每個類別及每個 Python 直譯器;不代表所有程式或程序都會看到同一個全域物件。範例會使用 first is second 檢查兩個名稱是否指向同一個物件。
使用裝飾器實作 Singleton
裝飾器可以將類別替換成一個函式,並由該函式快取第一次建立的實例。這種做法簡潔,也讓快取緊鄰受裝飾的類別。代價是公開名稱現在指向包裝函式,而不是原本的類別。透過 Sample 呼叫類別方法等類別層級操作,可能不再如預期運作;檢查類別的工具也會看到介面已經改變。
執行以下範例:
本節中的程式碼以及顯示的標籤或輸出仍為英文。
def singleton(class_):
instances = {}
def get_instance(*args, **kwargs):
if class_ not in instances:
instances[class_] = class_(*args, **kwargs)
return instances[class_]
return get_instance
@singleton
class Sample:
def __init__(self, name="shared"):
self.name = name
first = Sample()
second = Sample("ignored after construction")
print(first is second)
print(second.name)
提供的執行檢查已通過。第一次呼叫會建立實例,之後的呼叫都會回傳該實例,因此後續建構引數不會讓它重新初始化。輸出確認兩個名稱指向同一個物件,而且它的名稱仍是 shared。
使用基底類別實作 Singleton
基底類別可以在 __new__ 中集中處理實例建立;__new__ 會先於 __init__ 執行。請依具體子類別儲存實例,避免不同子類別意外共用同一個物件。由於 Python 在 __new__ 回傳後仍會呼叫 __init__,每次建構時都會執行初始化;若重複初始化會覆寫狀態,請讓初始化具備冪等性,或加上初始化防護。
本節中的程式碼以及顯示的標籤或輸出仍為英文。
class SingletonBase:
_instances = {}
def __new__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__new__(cls)
return cls._instances[cls]
class Cache(SingletonBase):
def __init__(self):
if not hasattr(self, "values"):
self.values = {}
first = Cache()
second = Cache()
print(first is second)
print(second.values)
這項防護可避免重複呼叫 __init__ 時取代現有字典。基底類別方案可能難以和其他繼承行為搭配,尤其是多個基底類別都自訂物件建立方式時。將 Singleton 行為納入可重複使用的類別階層前,請先考量這項代價。
使用元類實作 Singleton
元類會控制類別的建立方式。這個 Python 3 寫法會保留宣告出的類別,並為每個使用該元類的類別分別快取實例。呼叫類別時會執行它的 __call__ 方法。
本節中的程式碼以及顯示的標籤或輸出仍為英文。
class SingletonMeta(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Service(metaclass=SingletonMeta):
def __init__(self, endpoint="local"):
self.endpoint = endpoint
first = Service()
second = Service("another endpoint")
print(first is second)
print(second.endpoint)
相較於回傳函式的裝飾器,這種做法較能保留類別層級操作;不過,元類可能會與基底類別所需的另一個元類衝突。第一次呼叫時傳入的引數會用來初始化快取實例;之後的呼叫會直接回傳該實例,不再呼叫 __init__。此處的 metaclass=... 宣告使用 Python 3 語法;較舊的 Python 2 寫法並非目前的建議方式。
使用模組作為 Singleton
Python 會在直譯器內透過 sys.modules 快取匯入的模組。因此,以相同名稱匯入同一個模組時,呼叫端會取得相同的模組物件,並共用模組層級的狀態。如果共用資源本來就是一組函式與狀態,而不是具有類別介面的實例,這通常是最簡單的選擇。
本節中的程式碼以及顯示的標籤或輸出仍為英文。
import importlib
first = importlib.import_module("__main__")
second = importlib.import_module("__main__")
first.shared_value = "available through the module"
print(first is second)
print(second.shared_value)
這個簡短示範會以標準名稱匯入目前執行中的模組。在應用程式中,請將共用狀態與函式放在專用模組,並讓使用者程式一致地匯入該模組。模組快取的範圍限於單一直譯器:不同程序各有自己的模組物件,而重新載入模組可能會再次執行其中的程式碼。
選擇做法並了解其限制
若呼叫端需要共用函式或設定,可使用模組狀態;若需要控制物件建立流程並保留類別,可使用元類;若包裝介面可以接受,則可使用裝飾器。基底類別能透過繼承重複使用這種行為,但會增加初始化與繼承方面的限制。如果透過相依性注入傳入一般物件就能解決問題,通常會更容易測試與推理。
這些範例只展示一般單執行緒情況下的重複建構行為。若沒有同步建立流程,多個執行緒同時首次呼叫時可能發生競爭;而且每個程序或直譯器都有自己的快取。若資源必須在執行緒、程序或機器之間協調,請採用明確的同步機制或共用服務設計。只有在確實需要於指定範圍內共用單一實例時,才選用 Singleton。