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