首頁 > 軟體

詳解Python中__new__方法的作用

2022-03-31 13:02:53

前言

Python中類的構造方法__new__方法有何作用?

Python類中有些方法名、屬性名的前後都新增__雙下畫線,這種方法、屬性通常屬於Python的特殊方法和特殊屬性。通過重寫這些方法或直接呼叫這些方法來實現特殊功能。今天來聊聊構造方法__new__實際程式的應用場景。

我們知道常見的初始化__init__方法,可以重寫實現自己想要的初始化邏輯。最近實際業務開發過程中碰到一類問題比如資料資源載入快取機制的實現,用到了魔法方法中構造方法,其中__init__()和__new__是物件的構造器,合理運用將有效提高程式效能。

希望大家多結合自己的業務需求深刻理解,靈活運用,使得程式碼變得更加優雅。

一、__new__方法簡介

接下來通過範例逐步詳細闡述__ new __ 方法在類初始化過程中是什麼樣的存在!

1、初始化資料載入+解析類範例

class Solution(object):
    def __init__(self, name=None,data=None):
        self.name = name
        self.data = data
        #初始化載入資料
        self.xml_load(self.data)

    def xml_load(self,data):
        print("初始化init",data)

    def Parser(self):
        print("解析完成finish",self.name)

a = Solution(name="A111",data=10)
a.Parser()
b = Solution(name="A112",data=20)
b.Parser()
# print(a)與 print(b)返回了類的名稱和物件的地址
print(a)
print(b)
# 可以使用內建函數id()檢視python物件的記憶體地址
print(id(a))
print(id(b))

初始化init 10
解析完成finish A111
初始化init 20
解析完成finish A112
<__main__.Solution object at 0x0000024A3AF28D48>
<__main__.Solution object at 0x0000024A3B055C48>
2517839809864
2517841042504

注:

1、程式碼範例化類過程

一般使用__init__()方法初始化一個類的範例,當程式碼中範例化一個類的時候,第一個呼叫執行的是__new__()方法,當定義的類中沒有重新定義__new__()方法時候,Python會預設呼叫該父類別的__new__()方法來構造該範例,new方法就是先建立一個空間,然後每次建立一個範例化的物件,然後用開闢的空間存放這個範例化物件; 再次建立一個範例化的物件的時候,再用new方法開闢一個空間存放範例化物件。注意只有繼承了object的類才有此方法。

2、記憶體地址和物件可相互轉換

#通過_ctypes的api進行對記憶體地址的物件
import _ctypes
obj = _ctypes.PyObj_FromPtr(id(a))
#列印出來通過記憶體地址尋找到的物件
print(obj)

print(id(a))與 print(id(b))列印出來的都是記憶體地址(10進位制),print(a)與 print(b)返回了類的名稱和物件的地址,但是兩者並不相同。每次範例化類都會建立分配不同的物件地址,因此,程式碼範例化類過程中返回類物件的地址參照也就不同。

2、初始化資料載入重寫new方法+解析類範例

class Solution:
    """
    注:new方法是為範例化物件建立空間的方法,現在new方法被改寫,沒有將範例化物件參照返回給python的直譯器
    無法為範例化物件建立空間儲存,所以執行程式碼會報錯。也沒有完成初始化操作。
    """

    def __new__(cls, *args, **kwargs):
        print("物件建立空間")
        cls.instance = super().__new__(cls)
        print(cls.instance)
        # return cls.instance   #若未返回範例物件參照,範例化方法將報錯:AttributeError: 'NoneType' object has no attribute 'Parser'

    def __init__(self,name,data):
        self.name = name
        self.data = data
        self.xml_load(self.data)

    def xml_load(self,data):
        print("初始化init", data)

    def Parser(self):
        print("解析完成finish",self.data)

a = Solution("A111",10)
a.Parser()
print(id(a))

注:

1、__init__()方法和__new__()方法區別

__new__()方法用於建立範例,類範例化之前會首先呼叫,它是class的方法,是個靜態方法。而__init__()方法使用者初始化範例,該方法用在範例物件建立後被呼叫,它是範例物件的方法,用於設定類範例物件的一些初始值。

如果類中同時出現了__init__()方法和__new__()方法,則先呼叫__new__()方法後呼叫__init__()方法。__new__()方法是建立範例的第一步,執行完了需要返回建立的類的範例,否則則報錯,無法執行__init__()方法。其中,__init__()方法將不返回任何資訊。

2、重寫__new__()方法

def __new__(cls, *args, **kwargs):
    print(cls)  # cls 代表的是Solution這個類本身<class'__ main __.Solution'>
    cls.instance = super().__new__(cls)  # object().__ new __()
    print(cls.instance)
    return cls.instance

super()與object.__new__(cls)都是在呼叫父類別的new方法,必須把父類別的new方法返回給函數,才能開闢空間,因此必須新增return。程式碼的執行順序是:先執行new方法,然後執行init方法,最後是其它方法。

二、單例模式

單例模式最初的定義出現於《設計模式》:“保證一個類僅有一個範例,並提供一個存取它的全域性存取點。”

單例的使用主要是在需要保證全域性只有一個範例可以被存取的情況,比如系統紀錄檔的輸出、作業系統的工作管理員等。

1、用new方法如何實現單例模式

class Solution:
    # 1、記錄第一個被建立物件的參照,代表著類的私有屬性
    _instance = None # 靜態變數 儲存在類的名稱空間裡的

    def __init__(self,name,data):
        self.name = name
        self.data = data
        self.xml_load(self.data)

    def __new__(cls, *args, **kwargs):
        # 2.判斷該類的屬性是否為空;對第一個物件沒有被建立,我們應該呼叫父類別的方法,為第一個物件分配空間
        if cls._instance == None:  
            # 3.把類屬性中儲存的物件參照返回給python的直譯器
            cls._instance = object.__new__(cls)  # 3
            return cls._instance
        # 如果cls._instance不為None,直接返回已經範例化了的範例物件
        else:
            return cls._instance  # 必須把地址返回給new方法,讓它有儲存空間

    def xml_load(self,data):
        print("初始化init",self.name,data)

    def Parser(self):
        print("解析完成finish",self.name)

a = Solution("A11",10)  #第一次開闢一個物件空間地址,後面建立都是在該地址上進行的
a.Parser()
b = Solution("A12",20)  #b把a覆蓋掉
b.Parser()
print(id(a))
print(id(b))
# 記憶體地址,而且它們的記憶體地址都是一樣的
print(a.name)
print(b.name)

輸出

初始化init A11 10
解析完成finish A11
初始化init A12 10
解析完成finish A12
2465140199816
2465140199816
A12
A12 

注:

1、單例模式始終只有一個空間,該空間一直重複利用。

首先定義一個類的私有屬性_instance,用來記錄第一個被建立物件的參照,如果cls._instance為None說明該類還沒有範例化過,則範例化該類並返回範例物件。

通過以下資料測試可知,print(obj.name, obj.data)最後列印出來的都是A12,第一次列印"A11"時,屬性為空,執行if語句開闢了一個空間存放該屬性;從 第二次打已經開闢了空間 ,執行else語句,直接返回"A12"到原來的空間中,把前面的蓋資料覆蓋掉。

def task(id,data):
    obj = Solution("{0}".format(id), "{0}".format(data))
    print(obj.name, obj.data)

import threading
ID=["A11","A12","A13","A14","A12"]
DATA=[10,20,30,40,20]
for i in range(5):
    t = threading.Thread(target=task(ID[i],DATA[i]), args=[i, ])
    t.start()

輸出

<__main__.Solution object at 0x00000221B2129148>
初始化init A11 10
A11 10
初始化init A12 20
A12 20
初始化init A13 30
A13 30
初始化init A14 40
A14 40
初始化init A12 20
A12 20

2、單例模式另外一種實現方法 

def __new__(cls,*args,**kwargs):
    # hasattr查詢目標並判斷有沒有,not  1==1  返回的是False
    # if語句後面的
    # not 條件整體為True時,執行cls.instance = object....程式碼

    # if語句後面的
    # not 條件整體為False時,執行return程式碼
    if not hasattr(cls,"instance"):     # hasattr查、判斷的作用
        cls.instance = object.__new__(cls)
    return cls.instance

2、如何控制類僅執行一次初始化方法

以上實現了單例模式物件空間的重複利用,但是有時候我們想初始化過程只載入一次,避免頻繁請求浪費系統資源(如資料庫連線請求資料)。

class Solution:
    #定義類變數
    # 記錄第一個被建立物件的參照,代表著類的私有屬性
    _instance = None
    #記錄是否執行過初始化動作
    init_flag = False

    def __init__(self,name,data):
        self.name = name
        self.data = data
        #使用類名呼叫類變數,不能直接存取。
        if Solution.init_flag:
            return
        self.xml_load(self.data)
        # 修改類屬性的標記
        Solution.init_flag = True

    def __new__(cls, *args, **kwargs):
        # 判斷該類的屬性是否為空;對第一個物件沒有被建立,我們應該呼叫父類別的方法,為第一個物件分配空間
        if cls._instance == None: 
            # 把類屬性中儲存的物件參照返回給python的直譯器
            cls._instance = object.__new__(cls)  
            return cls._instance
        #如果cls._instance不為None,直接返回已經範例化了的範例物件
        else:
            return cls._instance 

    def xml_load(self,data):
        print("初始化init",self.name,data)

    def Parser(self):
        print("解析完成finish",self.name)

a = Solution("A11",10)  #第一次範例化物件地址,後面建立都是在該地址上進行的
a.Parser()
b = Solution("A12",20)  #b把a覆蓋掉
b.Parser()
print(id(a))
print(id(b))
print(a.name)
print(b.name)

輸出

初始化init A11 10
解析完成finish A11
解析完成finish A12
2280855720328
2280855720328
A12
A12 

注:

1、單例模式下僅載入一次初始化過程。

這時候我們在類空間中再新增一個init_flag屬性來記錄是否已經執行過初始化操作即可實現載入一次初始化過程。從以上兩次範例化過程結果來看,物件參照地址不變,結果被最後一次範例化資料覆蓋且初始化init只被列印一次。

2、單例模式下一次資源載入注意點

單例模式下控制類僅進行一次初始化過程適用於資源一次性載入進快取的過程,對於多程序應用可採用多例模式實現。

三、多例模式

多個範例物件空間參照地址完全獨立,從而保持避免不同請求資源不被佔用。將同一個物件請求歸為同一個範例。

class Solution:
    ##定義類範例化物件字典,即不同的範例物件對應不同的物件空間地址參照
    _loaded = {}

    def __init__(self,name,data):
        self.name = name
        self.data = data
        self.xml_load(self.data)

    def __new__(cls, name,*args):
        if cls._loaded.get(name) is not None:
            client = cls._loaded.get(name)
            print(f"已經存在存取物件 {name}")
            print(client)
            return client
        # 把類屬性中儲存的物件參照返回給python的直譯器
        print(f"正在建立存取物件 {name}")
        client = super().__new__(cls)
        # 為該類範例name新增一個空間物件地址參照
        print(client)    
        cls._loaded[name] = client
        return client

    def xml_load(self,data):
        print("初始化init",self.name,data)

    def Parser(self):
        print("解析完成finish",self.name)

if __name__ == '__main__':
    print("多例模式範例")
    a = Solution("A11",10)
    a.Parser()
    b = Solution("A11",10)
    b.Parser()
    c = Solution("A12", 20)
    c.Parser()
    print(f"{a is b}")
    print(a.name)
    print(b.name)
    print(c.name)

注:

1、多例模式始終具有多個空間,不同空間完全獨立。

我們在類空間中定義類範例化物件字典,即建立不同的範例物件和物件空間地址參照鍵值對,從而實現多例模式。通過類字典判斷範例物件是否建立,節省建立的成本。

2、多例模式測試過程

當建立相同的範例物件name="A11"時,程式首先在範例池中搜尋cls._loaded.get(name),若存在則直接返回已建立的範例物件空間。多例模式完美的實現了不同存取物件具體不同的範例化物件地址。

3、多例模式下緩衝機制的實現

進一步優化多例模式初始化過程,比如讀取檔案或者資料庫時僅進行一次初始化載入。

class Solution:
    ##定義類範例化物件字典,即不同的範例物件對應不同的物件空間地址參照
    _loaded = {}

    def __new__(cls, name,data,*args):
        if cls._loaded.get(name) is not None:
            client = cls._loaded.get(name)
            print(f"已經存在存取物件 {name}")
            print(client)
            return client
        print(f"正在建立存取物件 {name}")
        # 把類屬性中儲存的物件參照返回給python的直譯器
        client = super().__new__(cls)
        print(client)
        # 為該類範例name新增一個空間物件地址參照
        cls._loaded[name] = client
        client._init_db(name,data)
        return client

    def _init_db(self,name,data):
        self.name = name
        self.data = data
        self.xml_load(self.data)

    def xml_load(self,data):
        print("初始化init",self.name,data)

    def Parser(self):
        print("解析完成finish",self.name)

if __name__ == '__main__':
    print("多例模式範例-快取")
    a = Solution("A11",10)
    a.Parser()
    b = Solution("A11",10)
    b.Parser()
    c = Solution("A12", 20)
    c.Parser()
    print(f"{a is b}")
    print(a.name)
    print(b.name)
    print(c.name)

輸出

正在建立存取物件 A11
<__main__.Solution object at 0x0000024198989148>
初始化init A11 10
解析完成finish A11
已經存在存取物件 A11
<__main__.Solution object at 0x0000024198989148>
解析完成finish A11
正在建立存取物件 A12
<__main__.Solution object at 0x00000241989891C8>
初始化init A12 20
解析完成finish A12
True
A11
A11
A12

注:多例模式下多個範例化物件均只進行一次初始化過程。

重寫__new__方法中每個範例物件建立後繫結初始化_init_db()方法執行一次,後面遇到同一個範例物件將不會發生什麼,直接返回已建立的範例物件。從測試結果來看,建立相同的範例物件name="A11"時,第二次將略過初始化資料載入過程,很好的實現了快取機制。

總結

本文結合專案背景詳細介紹了__new__方法實現單例模式和多例模式以及快取機制的實現!

1、__new__ 方法是在類建立範例的時候自動呼叫的。

2、 範例是通過類裡面的 __ new __ 方法是在類建立出來的。

3、 先呼叫__new__ 方法建立範例,再呼叫 __ init __方法初始化範例。

4、 __new__ 方法,後面的括號裡面的cls代表的是類本身。

5、__new__ 方法,判斷類屬性為空就去開闢空間,否則複用原來的地址。

更多的特殊方法比如1、自我描述方法:__repr__2、解構方法:__del__ 3、列出物件所有屬性(包括方法)名:__dir__4、__dict__屬性:檢視物件內部所有屬性名和屬性值組成的字典5、__getattr____setattr__等。

當然還有metaclass類的__new__方法,可以動態修改程式中的一批類,這個功能在開發一些基礎性的框架時非常有用,可以使用metaclass為某一批需要通用功能的類新增方法。

以上就是詳解Python中__new__方法的作用的詳細內容,更多關於Python __new__的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com