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