1. 程式人生 > 程式設計 >python 6種方法實現單例模式

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 單例模式的資料請關注我們其它相關文章!