一文解密Python的弱引用
本次我們來聊一聊對象的弱引用,但在此之前,我們首先需要了解 Python 中的引用計數(shù)。
引用計數(shù)
Python 的變量本質(zhì)上是一個 PyObject * 泛型指針,它是一個和對象關(guān)聯(lián)的名字,我們通過這個名字可以找到其引用的對象。比如 a = 666,可以理解為 a 引用了 666 這個對象,而一個對象被多少個變量引用,那么該對象的引用計數(shù)就是多少。
同理如果 b = a,那么代表 b 也引用了 a 所引用的對象。因此 b = a 之后,兩個變量沒有什么直接的關(guān)系,只是這兩個變量都引用了同一個對象罷了,而此時 666 這個整數(shù)對象的引用計數(shù)就是 2。
當我們 del a 之后,并不代表要刪除 666 這個對象,只是將 a 這個變量給刪除了,讓 a 不再引用 666 這個對象,但是 b 還在引用它。如果 del b 之后,那么 b 也不再引用 666 這個對象了,所以此時它的引用計數(shù)變成了 0,而一旦一個對象的引用計數(shù)變成了 0,那么它就會被 Python 解釋器給回收掉。
class?A: ????def?__init__(self,?obj): ????????self.obj?=?obj ????def?__del__(self): ????????print("當實例對象被回收時,?會觸發(fā)我的執(zhí)行······") #?顯然我們創(chuàng)建了一個對象?A(123),?然后讓變量?a?指向(引用)它 #?然后?b?=?a,?讓?b?也指向?a?指向的對象 a?=?A(123) b?=?a #?此時對象的引用計數(shù)為?2,?然后我們將?a?刪除掉 del?a print("無事發(fā)生,?一切正常") #?如果再?del?b,?那么?A(123)?的引用計數(shù)就變成了?0,?那么它就該被回收了 #?一旦被回收,?就會觸發(fā)析構(gòu)函數(shù)?__del__ del?b print("觸發(fā)完析構(gòu)函數(shù),?這里會打印") """ 無事發(fā)生,?一切正常 當實例對象被回收時,?會觸發(fā)我的執(zhí)行······ 觸發(fā)完析構(gòu)函數(shù),?這里會打印 """
所以對象是否被回收的唯一準則就是它的引用計數(shù)是否為 0,只要為 0 就被回收。然而引用計數(shù)雖然簡單、也比較直觀,但是它無法解決循環(huán)引用的問題。
循環(huán)引用
什么是循環(huán)引用呢?
class?A: ????def?__init__(self,?obj): ????????self.obj?=?obj ????def?__del__(self): ????????print("當實例對象被回收時,?會觸發(fā)我的執(zhí)行······") #?創(chuàng)建兩個對象,?分別讓?a?和?b?引用 a?=?A(123) b?=?A(123) #?然后,?重點來了 a.obj?=?b b.obj?=?a #?此時?a?引用的實例對象被?b.obj?引用了 #?b?引用的實例對象被?a.obj?引用了 #?這個時候,兩個對象的引用計數(shù)都為?2 #?然后我們?del?a,?b,這個時候能把對象刪除掉嗎??顯然是不能的 #?因為它們的引用計數(shù)都變成了 1, 不是?0。只要不為?0, 就不會被回收 del?a,?b
以上這種情況被稱之為循環(huán)引用,而這也正是引用計數(shù)機制所無法解決的痛點,所以 Python 的 gc 就出現(xiàn)了,它的目的正是為了解決循環(huán)引用而出現(xiàn)的。
上面這段程序其實執(zhí)行之后,兩個對象還是會被回收的,因為程序一旦結(jié)束,Python 會釋放所有對象。當然即便程序不結(jié)束,我們在 del a, b 之后,對象也會被刪掉,只不過需要等到 gc 發(fā)動的時候了。因為 Python 的 gc 可以找出那些發(fā)生循環(huán)引用的對象,并減少它們的引用計數(shù)。
import?gc class?A: ????def?__init__(self,?obj): ????????self.obj?=?obj ????def?__del__(self): ????????print("當實例對象被回收時,?會觸發(fā)我的執(zhí)行······") a?=?A(123) b?=?A(123) a.obj?=?b b.obj?=?a del?a,?b print("析構(gòu)函數(shù)沒有被執(zhí)行,?因為引用計數(shù)不為零") #?gc?觸發(fā)是需要條件的,?但是?Python?支持我們手動引發(fā)?gc #?gc?發(fā)動之后會找出發(fā)生循環(huán)引用的對象 #?由于這里的兩個對象沒有外部的變量引用,?所以它們都是要被回收的 gc.collect() print("兩個對象都被回收了") """ 析構(gòu)函數(shù)沒有被執(zhí)行,?因為引用計數(shù)不為零 當實例對象被回收時,?會觸發(fā)我的執(zhí)行······ 當實例對象被回收時,?會觸發(fā)我的執(zhí)行······ 兩個對象都被回收了 """
所以 Python 的垃圾回收機制就是為了解決循環(huán)引用的,從根節(jié)點出發(fā),采用三色標記模型對 Python 對象進行標記清除,找出可達與不可達的對象。凡是不可達的對象,說明已經(jīng)沒有外部變量引用它們了。
就比如代碼中的兩個對象已經(jīng)沒有外部引用了,因為 a 和 b 兩個變量都已被刪除,但由于這兩個老鐵還在彼此抱團取暖,導致引用計數(shù)機制沒有識別出來。而當垃圾回收的時候,垃圾回收器會找到發(fā)生循環(huán)引用的對象,并手動將它們的引用計數(shù)減一。所以上面在 gc.collect() 之后,它們的引用計數(shù)就從 1 變成了 0,因此就被回收了。
但需要注意的是,對象是否被回收取決于它的引用計數(shù)是否為 0,而垃圾回收只是負責修正引用計數(shù),讓引用計數(shù)機制能夠正常工作。
而且對于那些有能力產(chǎn)生循環(huán)引用的對象,解釋器都會將它們掛在單獨的鏈表上,也就是所謂的零代鏈表、一代鏈表、二代鏈表。垃圾回收器會負責定期檢測這些鏈表,看是否有產(chǎn)生循環(huán)引用的對象,因此鏈表中的對象越多,那么檢測一次的代價就越大。
如果你能確保某個對象一定不會發(fā)生循環(huán)引用,那么也可以不讓它參與垃圾回收,當然只有在寫 C 擴展的時候才能這么做。
強引用與弱引用
Python 變量直接引用對象是強引用,會增加對象的引用計數(shù);而所謂弱引用,就是變量在引用一個對象的時候,不會增加這個對象的引用計數(shù)。
而 Python 也是支持弱引用的,對象的所有弱引用都會保存在該對象的一個字段里面。舉個例子:
對象本質(zhì)上是一個結(jié)構(gòu)體實例,結(jié)構(gòu)體內(nèi)部會有一個字段專門負責維護該對象的弱引用,從注釋可以看出這個字段就是一個列表。
如何實現(xiàn)弱引用
如果想實現(xiàn)弱引用,需要使用 weakref 模塊,一般來說這個模塊用的比較少,因為弱引用本身用的就不多。但是弱引用在很多場景中,可以發(fā)揮出很神奇的功能。
import?weakref class?RefObject: ????def?__del__(self): ????????print("del?executed") obj?=?RefObject() #?對象的弱引用通過?weakref.ref?類來創(chuàng)建 r?=?weakref.ref(obj) print(obj)? """ <__main__.RefObject?object?at?0x000001B7C573A5E0> """ #?顯示關(guān)聯(lián)?RefObject print(r) """ <weakref?at?0x000001B7DCAE19A0;?to?'RefObject'?at?0x000001B7C573A5E0> """ #?對引用進行調(diào)用的話,?即可得到原對象 print(r()?is?obj)?? """ True """ #?刪除?obj?會執(zhí)行析構(gòu)函數(shù) del?obj?? """ del?executed """ #?之前說過?r()?等價于?obj,?但是obj被刪除了,?所以返回?None #?從這里返回?None?也能看出這個弱引用是不會增加引用計數(shù)的 print("r():",?r())? """ r():?None """ #?打印弱引用,?告訴我們狀態(tài)已經(jīng)變成了?dead print(r)?? """ <weakref?at?0x000001B7DCAE19A0;?dead> """
通過弱引用我們可以實現(xiàn)緩存的效果,當弱引用的對象存在時,則對象可用;當對象不存在時,則返回 None,程序不會因此而報錯。這個和緩存本質(zhì)上是一樣的,也是一個有則用、無則重新獲取的技術(shù)。
此外 weak.ref 還可以接受一個可選的回調(diào)函數(shù),刪除引用所指向的對象時就會調(diào)用這個回調(diào)函數(shù)。
import?weakref class?RefObject: ????def?__del__(self): ????????print("del?executed") obj?=?RefObject() r?=?weakref.ref(obj,?lambda?ref:?print("引用被刪除了",?ref)) del?obj?? print("r():",?r())? """ del?executed 引用被刪除了?<weakref?at?0x0000021A69681900;?dead> r():?None """ #?回調(diào)函數(shù)會接收一個參數(shù),?也就是死亡之后的弱引用;?
前面我們說了,對象的弱引用會由單獨的字段保存,也就是保存在列表中。當對象被刪除時,會遍歷這個列表,依次執(zhí)行弱引用綁定的回調(diào)函數(shù)。
創(chuàng)建弱引用除了通過 weakref.ref 之外,還可以使用代理。有時候使用代理比使用弱引用更方便,使用代理可以像使用原對象一樣,而且不要求在訪問對象之前先調(diào)用代理。這說明,可以將代理傳遞到一個庫,而這個庫并不知道它接收的是一個代理而不是一個真正的對象。
import?weakref class?RefObject: ????def?__init__(self,?name): ????????self.name?=?name ????def?__del__(self): ????????print("del?executed") obj?=?RefObject("my?obj") r?=?weakref.ref(obj) p?=?weakref.proxy(obj) #?可以看到引用加上()才相當于原來的對象 #?而代理不需要,直接和原來的對象保持一致 print(obj.name)??#?my?obj print(r().name)??#?my?obj print(p.name)??#?my?obj #?但是注意:?弱引用在調(diào)用之后就是原對象,?而代理不是 print(r()?is?obj)??#?True print(p?is?obj)??#?False del?obj??#?del?executed try: ????#?刪除對象之后,?再調(diào)用引用,?打印為None ????print(r())??#?None ????#?如果是使用代理,?則會報錯 ????print(p) except?Exception?as?e: ????print(e)??#?weakly-referenced?object?no?longer?exists
weakref.proxy 和 weakref.ref 一樣,也可以接收一個額外的回調(diào)函數(shù)。
字典的弱引用
weakref 專門提供了 key 為弱引用或 value 為弱引用的字典,先來看看普通字典。
class?A: ????def?__del__(self): ????????print("__del__") a?=?A() #?創(chuàng)建一個普通字典 d?=?{} #?由于?a?作為了字典的?key,?那么?a?指向的對象引用計數(shù)會加?1,?變成?2 d[a]?=?"xxx" #?刪除?a,?對對象無影響,?不會觸發(fā)析構(gòu)函數(shù) del?a print(d) """ {<__main__.A?object?at?0x000002092669A5E0>:?'xxx'} __del__ """ #?最后打印的?__del__?是程序結(jié)束時,?將對象回收時打印的
但如果是對 key 為弱引用的字典的話,就不一樣了。
import?weakref class?A: ????def?__del__(self): ????????print("__del__") a?=?A() #?創(chuàng)建一個弱引用字典,?它的?api?和普通字典一樣 d?=?weakref.WeakKeyDictionary() print("d:",?d)?? """ d:?<WeakKeyDictionary?at?0x7f8a581a0d30> """ #?此時?a?指向的對象的引用計數(shù)不會增加 d[a]?=?"xxx" print("before?del?a:",?list(d.items())) """ before?del?a:?[(<__main__.A?object?at?0x7f8a581a0d60>,?'xxx')] """ #?刪除?a,?對象會被回收 del?a """ __del__ """ print("after?del?a:",?list(d.items())) """ after?del?a:?[] """
key 為弱引用的字典不會增加 key 的引用計數(shù),并且當對象被回收時,會自動從字典中消失。
除了可以創(chuàng)建 key 為弱引用的字典,還可以創(chuàng)建 value 為弱引用的字典。
import?weakref class?A: ????def?__del__(self): ????????print("__del__") a?=?A() d?=?weakref.WeakValueDictionary() #?value?為弱引用 d["xxx"]?=?a print("before?del?a:",?list(d.items())) """ before?del?a:?[('xxx',?<__main__.A?object?at?0x7f89580a7d60>)] """ #?刪除?a,?對象會被回收 del?a """ __del__ """ print("after?del?a:",?list(d.items())) """ after?del?a:?[] """
整個過程是一樣的,當對象被回收時,鍵值對會自動從字典中消失。
除了字典,我們還可以創(chuàng)建弱引用集合,將對象放入集合中不會增加對象的引用計數(shù)。
import?weakref class?A: ????def?__del__(self): ????????print("__del__") a?=?A() s?=?weakref.WeakSet() s.add(a) print(len(s)) del?a print(len(s)) """ 1 __del__ 0 """
讓自定義類支持弱引用
每一個自定義類的實例,都會有自己的屬性字典 __dict__。而我們知道字典使用的是哈希表,這是一個使用空間換時間的數(shù)據(jù)結(jié)構(gòu),因此如果想省內(nèi)存的話,那么我們通常的做法是指定 __slots__ 屬性,這樣實例就不會再有屬性字典 __dict__ 了。
import?weakref class?A: ????__slots__?=?("name",?"age") ????def?__init__(self): ????????self.name?=?"古明地覺" ????????self.age?=?17 a?=?A() try: ????weakref.ref(a) except?Exception?as?e: ????print(e)??#?cannot?create?weak?reference?to?'A'?object try: ????weakref.proxy(a) except?Exception?as?e: ????print(e)??#?cannot?create?weak?reference?to?'A'?object try: ????d?=?weakref.WeakSet() ????d.add(a) except?Exception?as?e: ????print(e)??#?cannot?create?weak?reference?to?'A'?object
此時我們發(fā)現(xiàn),A 的實例對象沒辦法被弱引用,因為指定了 __slots__。那么要怎么解決呢?很簡單,直接在 __slots__ 里面加一個屬性就好了。
import?weakref class?A: ????#?多指定一個__weakref__,?表示支持弱引用 ????__slots__?=?("name",?"age",?"__weakref__") ????def?__init__(self): ????????self.name?=?"古明地覺" ????????self.age?=?17 a?=?A() weakref.ref(a) weakref.proxy(a) d?=?weakref.WeakSet() d.add(a)
沒有報錯,可以看到此時就支持弱引用了。
C 的角度來看強引用和弱引用
首先 C 源代碼變成可執(zhí)行文件會經(jīng)歷如下幾個步驟:
- 預(yù)處理:進行頭文件展開,宏替換等等;
- 編譯:通過詞法分析和語法分析,將預(yù)處理之后的文件翻譯成匯編代碼,內(nèi)存分配也是在此過程完成的;
- 匯編:將匯編代碼翻譯成目標文件,目標文件中存放的也就是和源文件等效的機器代碼;
- 鏈接:程序中會引入一些外部庫,需要將目標文件中的符號與外部庫的符號鏈接起來,最終形成一個可執(zhí)行文件;
而在鏈接這一步,這些符號必須能夠被正確決議,如果沒有找到某些符號的定義,連接器就會報錯,這種就是強引用。而對于弱引用,如果該符號有定義,則鏈接器將該符號的引用決議,如果該符號未被定義,則鏈接器也不會報錯。
鏈接器處理強引用和弱引用的過程幾乎一樣,只是對于未定義的弱引用,鏈接器不認為它是一個錯誤的值。一般對于未定義的弱引用,鏈接器默認其為 0,或者是一個其它的特殊的值,以便于程序代碼能夠識別。
弱引用確實是一個比較復(fù)雜的地方,盡管 weakref 這個模塊用起來比較簡單,但是在解釋器層面,弱引用還是不簡單的。
到此這篇關(guān)于一文解密Python的弱引用的文章就介紹到這了,更多相關(guān)Python弱引用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
pyspark連接mysql數(shù)據(jù)庫報錯的解決
本文主要介紹了pyspark連接mysql數(shù)據(jù)庫報錯的解決,因為spark中缺少連接MySQL的驅(qū)動程序,下面就來介紹一下解決方法,感興趣的可以了解一下2023-11-113行Python代碼實現(xiàn)圖像照片摳圖和換底色的方法
這篇文章主要介紹了3行Python代碼實現(xiàn)圖像照片摳圖和換底色的方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-10-10react+django清除瀏覽器緩存的幾種方法小結(jié)
今天小編就為大家分享一篇react+django清除瀏覽器緩存的幾種方法小結(jié),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-07-07python 調(diào)用API接口 獲取和解析 Json數(shù)據(jù)
這篇文章主要介紹了python 如何調(diào)用API接口 獲取和解析 Json數(shù)據(jù),幫助大家更好的理解和使用python,感興趣的朋友可以了解下2020-09-09python 如何把docker-compose.yaml導入到數(shù)據(jù)庫相關(guān)條目里
這篇文章主要介紹了python 如何把docker-compose.yaml導入到數(shù)據(jù)庫相關(guān)條目里?下面小編就為大家介紹一下實現(xiàn)方式,一起跟隨小編過來看看吧2021-01-01Python使用jupyter notebook查看ipynb文件過程解析
這篇文章主要介紹了Python使用jupyter notebook查看ipynb文件過程解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-06-06工程師必須了解的LRU緩存淘汰算法以及python實現(xiàn)過程
這篇文章主要介紹了工程師必須了解的LRU緩存淘汰算法以及python實現(xiàn)過程,幫助大家更好的學習算法數(shù)據(jù)結(jié)構(gòu),感興趣的朋友可以了解下2020-10-10