欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文解密Python的弱引用

 更新時(shí)間:2023年09月14日 14:26:45   作者:古明地覺的編程教室  
弱引用在很多語言中都存在,最常用來解決循環(huán)引用問題,本文就來和大家一起探索一下python中的弱引用,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

本次我們來聊一聊對象的弱引用,但在此之前,我們首先需要了解 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)文章

最新評論