一 實例的創建過程
我們之前了解過在構造一個類的實例化對象時,會默認調用__init__方法,也就是類的初始化也叫構造函數,但其實在調用__init__方法前會首先調用__new__方法(只有在py3新式類才有)。即下面
- __new__(): 創建實例
作用: 在內存中分配對象空間 2 返回對象的引用self傳遞給init方法
? ? 2.__init__():? 初始化實例
-
當我們手動重寫這個方法后發現 構造函數沒有被調用了,并且調用test會報錯
-
此時我們調用查看構造的對象 ,發現它其實就是None
-
因為new方法被重寫了,并沒有創建對象。也沒有分配資源空間
class Test(object):def __init__(self):print("__init__")def __new__(cls, *args, **kwargs):print("__new__")def test(self):print("test")
test = Test()
test.test()
此時我們重寫__new__方法
1.1 重點(重寫__new__)
-
重寫__new__方法時一定要返回父類的__new__方法否則無法成功分配內存,
return super().__new__ -
這時候發現首先調用了__new__方法,然后調用了__init__方法。并且成功創建了實例對象
class Test(object):def __init__(self):print("__init__")def __new__(cls, *args, **kwargs):print("__new__")res = super().__new__(cls,*args, **kwargs)return res
test = Test()
print(test)
接下來看看一個類實例化的過程
1.2 實例化過程
-
首先執行__new__(),如果沒有重寫__new__,默認調用object內的__new__返回一個實例對象
-
然后再去調用__init__去初始化對象
# __new__是創建對象,分配空間等, __init__是初始化對象
# __new__是返回對象引用,__init__是定義實例屬性
# __new__是類級別的方法,__init__是實例級別的方法
二 單例
單例是軟件23種設計模式之一,一種創建型設計模式,它確保一個類只有一個實例,并提供一個全局訪問點來訪問這個實例
2.1 單例創建的幾種方式
# 1 通過@classmethmod類方法
# 2 通過裝飾器
# 3 通過重寫__new__ (主要方法)
# 4 通過導入模塊
2.2 通過重寫__new__方法實現
2.2.1設計流程:
# 1 定義一個類屬性,初始值為None,用來記錄單例對象的引用
# 2 重寫__new__方法
# 3 進行判斷,如果類屬性是None,把__new__返回的對象引用保存進去
# 4 返回類屬性中記錄的對象引用
2.2.2代碼實現:
- 這時候會發現無論創建多少次實例對象,返回的內存地址的引用不變
class Sinstance(object):obj = None"""這是一個重寫__new__方法的單例類"""def __new__(cls, *args, **kwargs):if cls.obj is None:cls.obj = super().__new__(cls)return cls.objdef __init__(self):print("__init__")s = Sinstance()
s2 = Sinstance()
print(s)
print(s2)
2.2.3 線程安全問題
- 上面這種方式在遇到多線程訪問時就會出現線程不安全。
- 兩個線程可能同時執行到if cls.obj is None:這一行檢查,發現cls.obj都為None,然后各自創建一個新實例,這就破壞了單例模式的目標。
- 為了確保在多線程環境下的線程安全性,你需要引入某種形式的同步機制來防止多個線程同時進入創建實例的代碼塊。最常見的做法是使用鎖(Lock)。
- 這種情況只會在第一次創建對象時有加鎖解鎖的額外開銷,并不會對性能有太大的影響
在這個版本中,我們使用了雙重檢查鎖定模式:首先不加鎖進行一次檢查,如果
obj
還未被初始化,則獲取鎖后(上鎖)(再此檢查是擔心有別的線程在這個線程還會加鎖的時候完成了實例創建),再檢查一次后,并在此時真正地創建實例。這樣做不僅保證了線程安全性,還提高了性能,因為大多數情況下不會進入加鎖的代碼段。只有當obj
確實為None
時,才會嘗試獲取鎖并再次檢查是否需要創建實例。這樣可以減少鎖的競爭,從而提高并發性能。
import threadingclass Sinstance(object):_lock = threading.Lock() # 創建一個鎖對象obj = Nonedef __new__(cls, *args, **kwargs):if cls.obj is None:with cls._lock: #在此處加鎖if cls.obj is None: #雙重檢查鎖定,避免不必要的加鎖開銷cls.obj = object.__new__(cls)return cls.objdef __init__(self):print('__init__')
2.2.4 測試
線程安全
- 這是一個創建10個線程來獲取單例的方法,打印發現他們的地址引用是同一個
- 則說明這種是線程安全的
# 測試函數:獲取單例實例并打印其ID
def test_singleton():instance = Sinstance()print(f"Instance ID: {id(instance)}")threads = []
for _ in range(10):t = threading.Thread(target=test_singleton)threads.append(t)t.start()
# 等待所有線程完成
for t in threads:t.join()
線程不安全
import threading
import time
class Sinstance(object):obj = Nonedef __new__(cls, *args, **kwargs):if cls.obj is None:# 模擬一些工作負載time.sleep(0.001)cls.obj = object.__new__(cls)return cls.objdef __init__(self):print('__init__')# 測試函數:獲取單例實例并打印其ID
def test_singleton():instance = Sinstance()print(f"Instance ID: {id(instance)}")threads = []
for _ in range(10):t = threading.Thread(target=test_singleton)threads.append(t)t.start()
# 等待所有線程完成
for t in threads:t.join()
- 經過測試如果不加鎖,確實線程是不安全的
2.3 通過模塊導入的方式
利用模塊導入的方式實現單例模式,在Python中實際上是一種非常簡單且線程安全的方法。這是因為在Python中,模塊在第一次被導入時會執行其頂層代碼,并且Python的模塊導入機制保證了每個模塊只會被加載一次,即使多次導入同一個模塊,也只會執行一次模塊中的代碼。這種特性天然地支持了單例模式的需求。
- 這是因為 Python 的模塊只會被加載一次,即使你多次導入同一個模塊,
- 在后續的導入操作中,Python只是重復使用已經加載的模塊對象。
- 這個在多線程的方式下是安全的.
首先我們再pymodule.py文件中創建這個te實例對象
import threading
import time
import threadingclass Sinstance(object):_lock = threading.Lock() # 創建一個鎖對象obj = Nonedef __new__(cls, *args, **kwargs):if cls.obj is None:time.sleep(0.001)with cls._lock: #在此處加鎖if cls.obj is None: #雙重檢查鎖定,避免不必要的加鎖開銷cls.obj = object.__new__(cls)return cls.objdef __init__(self):print('__init__')
te = Sinstance()
接著我們再另一個py文件里調用
from pymodule import te as instance01
from pymodule import te as instance02
print(instance01)
print(instance02)
- 這個是線程安全的
2.4 應用場景
# 1 系統緩存/軟件內部配置
# 2 數據庫連接池
# 3 任務調度器