目錄
一. 基于模塊的實現(簡單,易用)
?二. 重新創建時報錯(不好用)
三. 只靠方法獲取實例(不好用)
四. 類裝飾器
五. 重寫__new__方法
六. 元類
七. 總結
單例模式(Singleton Pattern)是一種設計模式,其核心目標是確保一個類只有一個實例存在,并提供一個全局訪問點。這種模式在需要控制資源訪問,節省系統資源或確保全局一致性的場景中非常有用。下面談一談Python中6種實現單例的方案,復雜程度基本上是由易到難的。除了第一種方案,剩下的在多線程環境中都有風險。如果你需要多線程單例,請注意加鎖!
一. 基于模塊的實現(簡單,易用)
模塊只有在第一次導入時會被初始化,后續導入直接使用已加載的模塊。這讓Python模塊成為了天然的單例,借助它即可輕松獲取一個唯一實例:
class _Singleton:'''一個不開放的單例類'''def __init__(self):self._value = '俺是單例'singleton = _Singleton()
然后,需要用到這個單例的地方直接導入現成的實例:
from module import singletonx = singleton
y = singleton
# x 和 y 是同一個對象嗎?
print(x is y) # True
像math.pi,math.e等的單例效果就是依此實現的。?
?二. 重新創建時報錯(不好用)
我們可以自己造一個異常來拒絕多次創建實例,當然不造用現成的也可以。比如:
class SingletonError(Exception):'''不能為單例類創建多個實例'''class 孤狼:_instance = Nonedef __init__(self, age):self.age = age# 第一次創建實例時,_instance 為 None,不報錯。# 第二次創建實例時,_instance 不為 None,直接報錯。if self.__class__._instance is not None:raise SingletonError('爺是孤狼,一山不容二虎!')self.__class__._instance = self狼大 = 孤狼(5)
狼二 = 孤狼(4)
在這個世界里,不能存在狼二,更別說光頭弱了:
?不過,其實新的實例已經被創造出來了。只是在初始化的時候強制程序報錯,把這個對象直接“扼殺在搖籃中”了,沒能賦值給“狼二”。而且,這種方法就怕人家把異常捕獲了,那后面會發生什么就不是我們能預測的了。
三. 只靠方法獲取實例(不好用)
在這種方案下,我們必須摒棄傳統的實例創建方法,轉而利用一個類方法獲取實例。
class 孤狼:def __init__(self, age):self.age = age# 必須完全使用這個方法來獲取實例@classmethoddef get_instance(cls, age):# 如果沒有實例化過,就創建一個實例if not hasattr(cls, '_instance'):cls._instance = cls(age)# 如果已經創建過實例,就返回這個實例return cls._instance狼大 = 孤狼.get_instance(5)
狼二 = 孤狼.get_instance(4)
print(狼二.age) # 5,而不是4
print(狼大 is 狼二) # True
這里并沒有真正拒絕像 "孤狼(參數)"?這樣的調用方式,要想完全拒絕這種調用,就繞回第二種方案了。因此這種方法又雞肋又不好用。
你可能會覺得:方案二,三是在搞笑嗎?嗯……這種活兒確實不應該用初級編程方法來干,下面我們看剩下的用元編程技巧實現的三種方案。
四. 類裝飾器
這種方案是用一個工廠函數取代原來的類,直接看實現方式。不過要說明一下,如果要實現單例的類是不可哈希的,就要把使用的鍵從類本身改為類名。不過我沒有這么干,因為單例一般就是不可變的。
from functools import wrapsdef singleton(cls):_instances = {}@wraps(cls)def wrapper(*args, **kwargs):if cls not in _instances:_instances[cls] = cls(*args, **kwargs)return _instances[cls]return wrapper@singleton
class 孤狼:def __init__(self, age):self.age = age
@singleton
class 圓頭耄耋:def __init__(self):self.標志技能 = '哈氣'狼大 = 孤狼(5)
狼二 = 孤狼(4)
print(狼二.age) # 5,而不是4
print(狼大 is 狼二) # True貓爹 = 圓頭耄耋()
貓爺 = 圓頭耄耋()
print(貓爹 is 貓爺) # True
下面解釋一下這個類裝飾器。
from functools import wrapsdef singleton(cls): #(1)_instances = {} #(2)@wraps(cls) #(3)def wrapper(*args, **kwargs): #(4)if cls not in _instances:_instances[cls] = cls(*args, **kwargs)return _instances[cls]return wrapper #(1)
(1):這個類裝飾器以@singleton使用,就相當于編寫了 cls = singleton(cls)。會將返回的內層函數賦值給類,讓類成為內層函數的引用。
(2):這個_instances字典在內層函數的閉包空間內,內層函數可以直接操作它。
(3):就算是單例,使用@wraps保存元數據也是個好習慣!
(4):內層函數現在“奪舍”了類,接受任意參數。如果類不在_instances中,說明還沒有為它創建實例,那就創建一個放到_instances中,最后返回的是_instances中的實例。如果不是首次創建,if條件檢查就不會通過,最終返回的是第一次創建的實例。
也可以給類新填一個類屬性存儲實例,后面元類方案我會展示這兩種不同的實現策略。這應該是三種元編程方案中最好的,__new__不夠靈活,元類太深奧。
五. 重寫__new__方法
__new__方法掌管實例的創建,而不是__init__。更具體地,實例先由__new__創建,然后,如果創建的東西確實是本類的實例,就作為self傳給__init__進行一系列屬性的賦值,完成初始化。如果不是本類的實例(真的可以這樣),就不交給__init__。
不過,實現單例只需要確保每次獲取的是同一個實例即可,不用擔心“生的孩子不是自己的”。下面看具體實現方法:
class 孤狼:_instance = Nonedef __new__(cls, *args, **kwargs):if not cls._instance:cls._instance = super().__new__(cls)return cls._instancedef __init__(self, age):self.age = age狼大 = 孤狼(5)
狼二 = 孤狼(4)
print(狼大.age) # 4
print(狼二.age) # 4
print(狼大 is 狼二) # True
這種使用_instance的技巧我們已經見過多次了,我就不多解釋了。創建實例是委托超類完成的,也就是super().__new__(cls),不需要傳入其他參數——這些參數其實就是__init__那里的參數,只不過“生自家孩子”往往用不到罷了。
那為什么這次反而是狼大的age被狼二覆蓋了?因為屬性age是在__init__中進行賦值的,創建狼二時是最后一次賦值,賦的值是4,所以這個單例的age值從5變成了4。
想要拒絕這種行為,可以在__init__中新增一個條件判斷,這時就又是狼大強壓狼二了:
class 孤狼:_instance = Nonedef __new__(cls, *args, **kwargs):if not cls._instance:cls._instance = super().__new__(cls)return cls._instancedef __init__(self, age):# 如果沒有設置name屬性,則設置它if not hasattr(self, 'age'):self.age = age狼大 = 孤狼(5)
狼二 = 孤狼(4)
print(狼大.age) # 5
print(狼二.age) # 5
print(狼大 is 狼二) # True
使用__new__其實很不靈活,對子類的支持不足——基本必須重寫子類的__new__方法。相比而言,類裝飾器和元類就能輕松支持任何類。
六. 元類
一般地,有其他方案我們就不會動用元類,一切類都是元類的實例,它是Python的“終極武器”和“黑魔法”。一來元類相對而言太高深了,二來元類的接口不一定就比其他方案好使。我就覺得類裝飾器超級好用呀!如果要在三種元編程方案中選一個,我肯定會選類裝飾器。
元類強大到可以干涉類的創建,初始化,和實例化三個過程。分別依賴元類的__new__,__init__,和__call__方法。現在我們想要插手實例創建的邏輯,應該在__call__上下功夫。
你會發現,下面的元類方案和類裝飾器方案很類似,都是在外部存儲了一個字典:
class MetaSingleton(type):_instances = {}def __call__(cls, *args, **kwargs):if cls not in MetaSingleton._instances:MetaSingleton._instances[cls] = super().__call__(*args, **kwargs)return MetaSingleton._instances[cls]class 孤狼(metaclass=MetaSingleton):def __init__(self, age):self.age = age狼大 = 孤狼(5)
狼二 = 孤狼(4)
print(狼大.age) # 5
print(狼二.age) # 5
print(狼大 is 狼二) # True
只不過,元類中的實例要委托超類type的__call__來創建,也就是super().__call__(*args, **kwargs)。使用元類的話,在定義類時指定metaclass=……就好了。
也可以把單例存儲在類屬性中,像下面這樣:
class MetaSingleton(type):def __call__(cls, *args, **kwargs):if not hasattr(cls, '_instances'):cls._instances = Noneif cls not in cls._instances:cls._instances = super().__call__(*args, **kwargs)return cls._instances
第一種方式缺點是占用的空間可能更大,而第二種方式缺點是給類新添了一個屬性,在做元編程時可能導致意外發生。?
七. 總結
推薦使用模塊單例或類裝飾器,方案二,三就是來搞笑的,剩下的兩種方案的話——__new__更復雜且不夠靈活好用;動用元類實現單例完全沒必要。只有非常少數的情況是非用元類不可的,我們對元類的態度往往是能不用就不用。如果你感興趣,我這里有一個真正需要元類出馬的簡單案例:
利用元類優化裝飾器接口的方案https://blog.csdn.net/2402_85728830/article/details/148046472