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

一文解密Python的弱引用

 更新時間:2023年09月14日 14:26:45   作者:古明地覺的編程教室  
弱引用在很多語言中都存在,最常用來解決循環(huán)引用問題,本文就來和大家一起探索一下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)文章

最新評論