python 6種方法實現單例模式
單例模式是一個軟體的設計模式,為了保證一個類,無論呼叫多少次產生的例項物件,都是指向同一個記憶體地址,僅僅只有一個例項(只有一個物件)。
實現單例模式的手段有很多種,但總的原則是保證一個類只要例項化一個物件,下一次再例項的時候就直接返回這個物件,不再做例項化的操作。所以這裡面的關鍵一點就是,如何判斷這個類是否例項化過一個物件。
這裡介紹兩類方式:
- 一類是通過模組匯入的方式;
- 一類是通過魔法方法判斷的方式;
# 基本原理: - 第一類通過模組匯入的方式,借用了模組匯入時的底層原理實現。 - 當一個模組(py檔案)被匯入時,首先會執行這個模組的程式碼,然後將這個模組的名稱空間載入到記憶體。 - 當這個模組第二次再被匯入時,不會再執行該檔案,而是直接在記憶體中找。 - 於是,如果第一次匯入模組,執行檔案原始碼時例項化了一個類,那再次匯入的時候,就不會再例項化。 - 第二類主要是基於類和元類實現,在'物件'的魔法方法中判斷是否已經例項化過一個物件 - 這類方式,根據實現的手法不同,又分為不同的方法,如: - 通過類的繫結方法;通過元類;通過類下的__new__;通過裝飾器(函式裝飾器,類裝飾器)實現等。
下面分別介紹這幾種不同的實現方式,僅供參考實現思路,不做具體需求。
通過模組匯入
# cls_singleton.py class Foo(object): pass instance = Foo() # test.py import cls_singleton obj1 = cls_singleton.instance obj2 = cls_singleton.instance print(obj1 is obj2) # 原理:模組第二次匯入從記憶體找的機制
通過類的繫結方法
class Student: _instance = None # 記錄例項化物件 def __init__(self,name,age): self.name = name self.age = age @classmethod def get_singleton(cls,*args,**kwargs): if not cls._instance: cls._instance = cls(*args,**kwargs) return cls._instance stu1 = Student.get_singleton('jack',18) stu2 = Student.get_singleton('jack',18) print(stu1 is stu2) print(stu1.__dict__,stu2.__dict__) # 原理:類的繫結方法是第二種例項化物件的方式, # 第一次例項化的物件儲存成類的資料屬性 _instance, # 第二次再例項化時,在get_singleton中判斷已經有了例項物件,直接返回類的資料屬性 _instance
補充:這種方式實現的單例模式有一個明顯的bug;bug的根源在於如果使用者不通過繫結類的方法例項化物件,而是直接通過類名加括號例項化物件,那這樣不再是單利模式了。
通過魔法方法__new__
class Student: _instance = None def __init__(self,age): self.name = name self.age = age def __new__(cls,**kwargs): # if cls._instance: # return cls._instance # 有例項則直接返回 # else: # cls._instance = super().__new__(cls) # 沒有例項則new一個並儲存 # return cls._instance # 這個返回是給是給init,再例項化一次,也沒有關係 if not cls._instance: # 這是簡化的寫法,上面註釋的寫法更容易提現判斷思路 cls._instance = super().__new__(cls) return cls._instance stu1 = Student('jack',18) stu2 = Student('jack',stu2.__dict__) # 原理:和方法2類似,將判斷的實現方式,從類的繫結方法中轉移到類的__new__中 # 歸根結底都是 判斷類有沒有例項,有則直接返回,無則例項化並儲存到_instance中。
補充:這種方式可以近乎完美地實現單例模式,但是依然不夠完美。不完美的地方在於沒有考慮到併發的極端情況下,有可能多個執行緒同一時刻例項化物件。關於這一點的補充內容在本文的最後一節介紹(!!!進階必會)。
通過元類**
class Mymeta(type): def __init__(cls,bases,dic): super().__init__(name,dic) cls._instance = None # 將記錄類的例項物件的資料屬性放在元類中自動定義了 def __call__(cls,**kwargs): # 此call會在類被呼叫(即例項化時觸發) if cls._instance: # 判斷類有沒有例項化物件 return cls._instance else: # 沒有例項化物件時,控制類造空物件並初始化 obj = cls.__new__(cls,**kwargs) obj.__init__(*args,**kwargs) cls._instance = obj # 儲存物件,下一次再例項化可以直接返回而不用再造物件 return obj class Student(metaclass=Mymeta): def __init__(self,age): self.name = name self.age = age stu1 = Student('jack',stu2.__dict__) # 原理:類定義時會呼叫元類下的__init__,類呼叫(例項化物件)時會觸發元類下的__call__方法 # 類定義時,給類新增一個空的資料屬性, # 第一次例項化時,例項化之後就將這個物件賦值給類的資料屬性;第二次再例項化時,直接返回類的這個資料屬性 # 和方式3的不同之處1:類的這個資料屬性是放在元類中自動定義的,而不是在類中顯示的定義的。 # 和方式3的不同之處2:類呼叫時觸發元類__call__方法判斷是否有例項化物件,而不是在類的繫結方法中判斷
函式裝飾器
def singleton(cls): _instance_dict = {} # 採用字典,可以裝飾多個類,控制多個類實現單例模式 def inner(*args,**kwargs): if cls not in _instance_dict: _instance_dict[cls] = cls(*args,**kwargs) return _instance_dict.get(cls) return inner @singleton class Student: def __init__(self,age): self.name = name self.age = age # def __new__(cls,**kwargs): # 將方法3的這部分程式碼搬到了函式裝飾器中 # if not cls._instance: # cls._instance = super().__new__(cls) # return cls._instan stu1 = Student('jack',stu2.__dict__)
類裝飾器
class SingleTon: _instance_dict = {} def __init__(self,cls_name): self.cls_name = cls_name def __call__(self,**kwargs): if self.cls_name not in SingleTon._instance_dict: SingleTon._instance_dict[self.cls_name] = self.cls_name(*args,**kwargs) return SingleTon._instance_dict.get(self.cls_name) @SingleTon # 這個語法糖相當於Student = SingleTon(Student),即Student是SingleTon的例項物件 class Student: def __init__(self,age): self.name = name self.age = age stu1 = Student('jack',stu2.__dict__) # 原理:在函式裝飾器的思路上,將裝飾器封裝成類。 # 程式執行到與語法糖時,會例項化一個Student物件,這個物件是SingleTon的物件。 # 後面使用的Student本質上使用的是SingleTon的物件。 # 所以使用Student('jack',18)來例項化物件,其實是在呼叫SingleTon的物件,會觸發其__call__的執行 # 所以就在__call__中,判斷Student類有沒有例項物件了。
!!!進階必會
本部分主要是補充介紹多執行緒併發情況下,多執行緒高併發時,如果同時有多個執行緒同一時刻(極端條件下)事例化物件,那麼就會出現多個物件,這就不再是單例模式了。
解決這個多執行緒併發帶來的競爭問題,第一個想到的是加互斥鎖,於是我們就用互斥鎖的原理來解決這個問題。
解決的關鍵點,無非就是將具體示例化操作的部分加一把鎖,這樣同時來的多個執行緒就需要排隊。
這樣一來只有第一個搶到鎖的執行緒例項化一個物件並儲存在_instance中,同一時刻搶鎖的其他執行緒再搶到鎖後,不會進入這個判斷if not cls._instance,直接把儲存在_instance的物件返回了。這樣就實現了多執行緒下的單例模式。
此時還有一個問題需要解決,後面所有再事例物件時都需要再次搶鎖,這會大大降低執行效率。解決這個問題也很簡單,直接在搶鎖前,判斷下是否有單例物件了,如果有就不再往下搶鎖了(程式碼第11行判斷存在的意義)。
import threading class Student: _instance = None # 儲存單例物件 _lock = threading.RLock() # 鎖 def __new__(cls,**kwargs): if cls._instance: # 如果已經有單例了就不再去搶鎖,避免IO等待 return cls._instance with cls._lock: # 使用with語法,方便搶鎖釋放鎖 if not cls._instance: cls._instance = super().__new__(cls,**kwargs) return cls._instance
以上就是python 6種方法實現單例模式的詳細內容,更多關於python 單例模式的資料請關注我們其它相關文章!