Python - 物件導向程式設計 - __new()__ 和單例模式

小菠蘿測試筆記發表於2021-09-10

單例模式

這是一種設計模式

  • 設計模式是前任工作的總結和提煉,通常,被人們廣泛流傳的設計模式都是針對某一特定問題的成熟的解決方案
  • 使用設計模式是為了可重用程式碼、讓程式碼更容易被他人理解、保證程式碼可靠性

 

單例設計模式

  • 目的:讓某一個類建立的例項物件,在整個應用程式中只有唯一的一個例項物件而且該物件易於外界訪問,從而方便對例項個數的控制並節約系統資源
  • 每一次執行 類名() 返回的物件,記憶體地址是相同的

 

單例設計模式的應用場景

  • 音樂播放器物件
  • 回收站物件
  • 印表機物件
  • .....

 

為什麼要單例模式?

  • 提問:如何保證一個類只有一個例項並且這個例項易於被訪問呢?
  • 不使用單例模式:定義一個全域性變數可以確保物件隨時都可以被訪問,但不能防止例項化多個物件
  • 單例模式的出現:類自己負責只能建立一個例項物件,可以保證沒有其他例項被建立,並且它可以提供一個訪問該例項的方法 

 

__new__ 方法

使用 類名() 建立物件時,Python 的直譯器首先會呼叫 __new__ 方法為物件分配記憶體空間

 

class PoloBlog:
    def __new__(cls, *args, **kwargs):
        print("分配記憶體地址啦")

    def __init__(self):
        print("初始化物件...")


blog = PoloBlog()
print(blog)


# 輸出結果
分配記憶體地址啦
None

哎,為什麼列印物件是 None,而且沒有呼叫到 __init__ 方法呢??下面講解!

 

內建的靜態方法

__new__ 是一個由 object 基類提供的內建的靜態方法

 

__new__ 主要作用

  • 在記憶體中為例項物件分配空間
  • 返回物件的引用給 Python 直譯器

Python 的直譯器獲得物件的引用後,將物件的引用作為第一個引數,傳遞給 __init__ 方法

 

重寫 __new__ 方法

  • 重寫的程式碼是固定的
  • 重寫 __new__ 方法一定要在最後 return super().__new__(cls) 
  • 如果不 return(像上面程式碼栗子一樣),Python 的直譯器得不到分配了空間的物件引用,就不會呼叫物件的初始化方法(__init__)
  • 重點:__new__ 是一個靜態方法,在呼叫時需要主動傳遞 cls 引數
class PoloBlog:
    def __new__(cls, *args, **kwargs):
        # 1、自動呼叫 __new__
        print("分配記憶體地址啦")
        # 2、為物件分配空間得到的引用賦值給 instance
        instance = super().__new__(cls)
        print(id(instance))
        # 3、返回物件引用給 Python 直譯器
        return instance

    def __init__(self):
        print("初始化物件...")
        print(id(self))


blog = PoloBlog()


# 輸出結果
分配記憶體地址啦
4363809888
初始化物件...
4363809888

可以看到列印的兩個記憶體地址是同一個哦:證明 __new__ 分配的物件引用的確傳給了 __init__ 方法的 self 引數

 

__new__ 實現單例模式

class PoloBlog:
    def __new__(cls, *args, **kwargs):
        print("分配記憶體地址啦")
        instance = super().__new__(cls)
        return instance

    def __init__(self):
        print("初始化物件...")


blog = PoloBlog()
blog1 = PoloBlog()

print(id(blog))
print(id(blog1))


# 輸出結果
4449363040
4449361984

很明顯,兩個物件各有自己的記憶體地址;單純的重寫 __new__ 方法並不能實現單例模式

 

__new__ 實現單例模式的邏輯

單例:在整個應用程式中只有唯一的一個例項物件

  1. 定義一個類屬性,來儲存單例物件的引用
  2. 重寫 __new__ 方法
  3. 如果類屬性 is None,則呼叫父類方法分配記憶體空間,並賦值給類屬性
  4. 如果類屬性已有物件引用,則直接返回

 

 

單例模式的程式碼實現

# 單例模式
class PoloBlog:
    instance = None

    def __new__(cls, *args, **kwargs):
        # 1、判斷類屬性是否為 None
        if cls.instance is None:
            # 2、為空,呼叫父類方法,給物件分配記憶體空間,並賦值給類屬性
            cls.instance = super().__new__(cls)

        # 3、如果不為空,則直接返回類屬性儲存的物件引用
        return cls.instance

    def __init__(self):
        pass


blog = PoloBlog()
blog1 = PoloBlog()
blog2 = PoloBlog()
print(id(blog), id(blog1), id(blog2))


# 輸出結果
4336982096 4336982096 4336982096

可以看到建立的三個例項物件其實都是同一個,這就是單例模式!

 

初始化工作僅執行一次

在每次使用類名()建立物件時,Python 的直譯器都會自動呼叫兩個方法

  • __new__ 分配空間
  • __init__ 物件初始化

上面所說的單例模式,是針對 __new__ 方法進行重寫的,建立多個例項物件都會得到同一個例項物件

但是:初始化方法還是會被多次呼叫

class PoloBlog:
    instance = None

    def __new__(cls, *args, **kwargs):
        if cls.instance is None:
     
            cls.instance = super().__new__(cls)

        return cls.instance

    def __init__(self):
        print("yep")


blog = PoloBlog()
blog1 = PoloBlog()
blog2 = PoloBlog()


# 輸出結果
yep
yep
yep

 

假設想讓初始化動作只執行一次呢?

其也很簡單,和單例模式的解決思路差不多

  1. 定義一個類屬性標記是否執行過初始化動作,初始值為 False
  2. 在 __init__ 方法中,判斷類屬性,如果 False,則執行初始化動作,然後設定為 True
  3. 如果 True 則直接跳過不執行
# 單例模式
class PoloBlog:
    instance = None
    init_flag = None

    def __new__(cls, *args, **kwargs):
        if cls.instance is None:
 
            cls.instance = super().__new__(cls)

        return cls.instance

    def __init__(self):
        # 1、判斷是否為 True,因為是例項方法,所以呼叫類屬性要通過類物件
        if PoloBlog.init_flag:
            # 2、如果 True,直接跳過不執行後續初始化動作
            return
        # 3、如果 False,則執行
        print("初始化動作")
        # 4、修改 init_flag
        PoloBlog.init_flag = True


blog = PoloBlog()
blog1 = PoloBlog()
blog2 = PoloBlog()


# 輸出結果
初始化動作

 

相關文章