原文:https://blog.csdn.net/NeverLate_gogogo/article/details/107021695
本文有刪改
前言
首先提一點:大家遇到python模塊的使用問題,盡可能去 python document去找答案。
但是關於weakref,官網上給的例子,並不能讓我們理解這個弱引用。
於是在網上查了一些資料,也是比較模糊。
於是我還是從變量到垃圾回收再到若弱引用講起這件事吧。因為他們是息息相關的,只有l理解了變量的引用和垃圾回收才會 更好的 理若引用的概念。
然后最后我再舉2個例子,說明弱引用是怎么體現出來的。
一、變量
1.1 變量是什么?
變量是一個對象別名,可以理解成變量是貼在對象上的一個標簽,所以當執行
my_list = [1,2,3,4]
其實就是在 [1,2,3,4]
這個對象上貼了一個標簽 my_list
,我們可以通過這個標簽來找到對象,進而可以操作對象。
那么我又執行:
my_list2 = my_list
這個時候發生了什么呢,其實就是又給[1,2,3,4]
這個對象添加了一個標簽。當然,這種說法是只限於對可變對象的操作。我們可以參考下圖:
每個變量都有 標識 、類型 和 值。對象一旦創建,它的標識絕不會變;可以把標識理解為對象在內存中的地址。is
運算符比較兩個對象的標識;id()
函數返回對象標識的整數表示。
因此當我們對上面執行 id(my_list1)
與 id(my_list)
他們返回的值是一樣的,因為他們指向同一個對象,my_list
與my_list1
只是標簽而已。
1.2 ==
和is
之間的比較
==
運算符比較兩個對象的值(對象中保存的數據),而 is
比較對象的標識。
is
運算符比 ==
速度快,因為它不能重載.接比較兩個對象的 整數 ID。
a == b
是語法糖,等同於 a.__eq__(b)
。
__eq__
方法繼承自 object
, 比較兩個對象的 ID,結果與 is
一樣。但是多數內置類型使用更有意義的方式覆蓋了 __eq__
方法,會考慮對象屬性的值。相等性測試可能涉及大量處理工作,例如,比較大型集合或嵌套層級深的結構時。
二、del與垃圾回收機制
這里我們只討論引用計數規則的垃圾回收機制
python中對象絕不會自行銷毀;然而,無法得到對象時,可能會被當作垃圾回收。無法得到對象包括兩種:
①沒有人引用這個對象了,也就是說這個對象身上被貼的標簽都沒有了,這時候我們其實就找不到這個對象了;
②相互引用
del
語句刪除名稱(也就是我們說的標簽),而不是對象。當我們把貼在對象身上的標簽全部刪除了,這時候python垃圾回收機制的引用計數(可以理解為貼標簽計數)檢測到引用此對象的次數為0,那么就觸發了垃圾回收機制,銷毀此對象。
因此del命令並不會刪除對象,而是當del刪除了對象的最后一個引用時,會觸發垃圾回收機制,回收器將對象銷毀。這個概念要搞清。(看例1的代碼)
重新綁定也可能會導致對象的引用數量歸零,導致對象被銷毀。什么意思呢?
我們執行下面的代碼:
my_list = [1,2,3,4]
my_list = [3,4,5,6]
這個時候,對象[1,2,3,4]
就被銷毀了,為什么?
因為my_list這個標簽從對象[1,2,3,4]
上被撕下來了,貼到了對象[3,4,5,6]
上。這時候對象[1,2,3,4]
的引用計數是不是就為0了,這時候就觸發垃圾回收機制,將[1,2,3,4]
這個對象給銷毀了。
所以說,重新綁定也可能導致對象被銷毀
(可以看例1的代碼)
# 代碼示例1
>>> import weakref
>>> s1 = {1,2,3} # 給對象{1,2,3}貼了一個標簽s1
>>> def bye(): # 對象{1,2,3}被銷毀時,調用這個函數
... print("拜拜,你被銷毀了")
...
>>> ender = weakref.finalize(s1,bye) # 綁定回調函數
>>> ender.alive # 看對象{1,2,3}仍然存活
True
>>> s1 = {4,5,6} # 當標簽s1從{1,2,3}上撕下來,對象{1,2,3}被銷毀了
拜拜,你被銷毀了
>>> ender.alive
False
如果兩個對象相互引用(不懂相互引用的可以自行學習一下),當它們的引用只存在二者之間時,垃圾回收程序會判定它們都無法獲取,進而把它們都銷毀。
__del__
特殊方法.不會銷毀實例,不應該在代碼中調用。即將銷毀實例時,Python 解釋器會調用 __del__
方法,給實例最后的機會,釋放外部資源。 參考標准庫del特殊方法.
在 CPython 中,垃圾回收使用的主要算法是引用計數。
實際上,每個對象都會統計有多少引用指向自己.
當引用計數歸零時,對象立即就被銷毀:CPython 會在對象上調用 __del__
方法(如果定義了),然后釋放分配給對象的內存。
CPython 2.0 增加了分代垃圾回收算法,用於檢測引用循環中涉及的對象組——如果一組對象之間全是相互引用,即使再出色的引用方式也會導致組中的對象不可獲取。
Python 的其他實現有更復雜的垃圾回收程序,而且不依賴引用計數,這意味着,對象的引用數量為零時可能不會立即調用 del 方法。
# 代碼示例2
# 使用 weakref.finalize 注冊一個在銷毀對象時調用的回調函數。
In [166]: import weakref
In [167]: s1 = {1,2,3}
In [168]: s2 = s1 # 指向同一個集合.
In [169]: def bye(): # 回調函數一定不能是要銷毀的對象的綁定方法(類方法),否則會有一個指向對象的引用。
...: print("拜拜,你被銷毀了")
...:
In [170]: ender = weakref.finalize(s1,bye) # 注冊回調, 並返回一個變量,判斷是否銷毀。
In [171]: ender.alive
Out[171]: True
In [172]: del s1
In [173]: ender.alive # 說明 del s1 是刪除引用,而不是對象。
Out[173]: True
In [174]: s2 = 'spam' # 重新綁定 s2 后,表示 {1,2,3} 無法獲取 (引用計數為0),此時調用 bye 回調函數.
拜拜,你被銷毀了
In [175]: ender.alive # ender 為 弱引用, 不在 計數 范圍內.
Out[175]: False
所以,每個引用就相當於一個標簽,通過這個標簽我們可以找到這個對象。一旦這些標簽別撕沒了,也就是對象的引用為0的時候,就出觸發python的垃圾回收的機制。
三、弱引用
這個時候可以引出我們心心念的弱引用了。
3.1 弱引用是什么?
在上文,我們看到,當執行 my_list=[1,2,3,4]
時,這時候就相當於給對象[1,2,3,4]
加了一個強引用(標簽)。再執行my_list1 = my_list
時,那么對象[1,2,3,4]
的強引用個數就變為了2,我們想要觸發python的垃圾回收機制銷毀對象[1,2,3,4]
時,就必須把兩個強引用my_list
和my_list1
都刪除。
這時候,my_list2 = [1,2,3,4]
這種方式,我不想使·my_list2·成為對象的強引用,那么我就可以把·my_list2·定義為一個弱引用,這時候,就當發生貼標簽的操作時,就會是一個弱引用。而弱引用不會影響垃圾回收的計數。也就是說,一個對象,只要強引用個數為0,就會觸發python的垃圾回收機制,而不管你有多少個弱引用,都是沒關系的。
3.2 弱引用介紹與使用
弱引用不會增加對象的引用數量。引用的目標對象稱為 所指對象 (referent)。因此,弱引用不會妨礙所指對象被當作垃圾回收。
弱引用在緩存應用中很有用,因為不想僅因為被緩存引用着而始終保存緩存對象。
使用 weakref.ref
實例可以獲取所指對象。如果對象存在,調用弱引用可以獲取對象;否則返回 None
。
weakref.ref
類其實是低層接口,供高級用途使用,多數程序最好使用 weakref 工具集 和 finalize
。
weakref 工具集合:
WeakKeyDictionary
:WeakValueDictionary
: 這是一種可變映射,里面的值是對象的弱引用。被引用的對象在程序中的其他地方被當作垃圾回收后,對應的鍵會自動從WeakValueDictionary
中刪除。因此,WeakValueDictionary
經常用於緩存。WeakSet
: 保存元素弱引用的集合類。元素沒有強引用時,集合會把它刪除。finalize
(內部使用弱引用)
如果一個類需要知道所有實例,一種好的方案是創建一個 WeakSet 類型的類屬性,保存實例的引用。
如果使用常規的 set
,實例永遠不會被垃圾回收,因為類中有實例的強引用,而類存在的時間與 Python 進程一樣長,除非顯式刪除類。
弱引用局限:
- 基本的 list 和 dict 實例不能作為所指對象, 但是它們的子類可以作為弱引用所指對象.
- 基本的 int 、 list 、 tuple 、string 、dict 實例不能作為弱引用的目標。
- 但是 set 實例可以作為所指對象。
- 但是, str 、 dict 、list 的子類實例 和 用戶自定義的類型實例 可以作為弱引用所指對象.
- 然而, int 、 tuple 的子類實例 也不能作為弱應用對象.
3.3 弱引用使用舉例
任何的數據結構都是可以弱引用的,我們要多利用weakref包中提供的工具類
# 前提: Python 控制台會自動把 _ 變量綁定到結果不為 None 的表達式結果上。
In [1]: import weakref
In [2]: a_set = {0,1}
In [3]: wref = weakref.ref(a_set) # 弱引用對象 wref
In [4]: wref
Out[4]: <weakref at 0x10f29aea8; to 'set' at 0x10e906f28>
In [5]: wref() # 返回的是被引用的對象,{0, 1}。因為是控制台會話,所以 {0, 1} 會綁定給 _ 變量。
Out[5]: {0, 1}
In [6]: a_set = {2, 3, 4} # a_set 不再指代 {0, 1} 集合,因此集合的引用數量減少了。但是 _ 變量仍然指代它。
In [7]: wref()
Out[7]: {0, 1}
In [8]: wref() is None # 計算這個表達式時,{0, 1} 存在,因此 wref() 不是 None。但是,隨后 _ 綁定到結果值 False。現在 {0, 1} 沒有強引用了。
Out[8]: False
In [9]: wref() is None # 因為 {0, 1} 對象不存在了,所以 wref() 返回 None。
Out[9]: Ture
WeakValueDictionary 示例:
class Cheese:
def __init__(self, kind):
self.kind = kind
def __repr__(self):
return 'Cheese(%r)' % self.kind
# 執行:
In [2]: import weakref
In [3]: stock = weakref.WeakValueDictionary() # 創建弱引用字典實例。
In [4]: catalog = [Cheese('Read Leicester'), Cheese('Tilsit'),Cheese('Brie'), Cheese('Parmesan')]
In [6]: for cheese in catalog:
...: stock[cheese.kind] = cheese # 名稱映射到實例. [弱引用]
...:
In [7]: sorted(stock.keys())
Out[7]: ['Brie', 'Parmesan', 'Read Leicester', 'Tilsit']
In [8]: del catalog
In [9]: sorted(stock.keys()) # 為什么還剩一個? 因為臨時變量。
Out[9]: ['Parmesan']
In [10]: del cheese
In [11]: sorted(stock.keys()) # 臨時變量刪除后,為空.
Out[11]: []
下面這個例子,是我自己編寫的,大家要搞懂下面的這個例子,相信對弱引用,就有些理解了。
>>> import weakref
>>>
>>> class C: # 這里新建一個類,因為WeakValueDictionary()
... def __init__(self, value): # 要求value是一個obj
... self.value = value
...
>>> def test_weak_value_dict():
... d= weakref.WeakValueDictionary()
... k1 = 'test1'
... v1 = C(1) # 這時候C(1)是有一個強引用的:v1
... d[k1] = v1 # 這個語句也就是字典賦值,但是由於我們用的
... print(d[k1]) # WeakValueDictionary(),所以字典里的是弱引用
... del v1 # 這時候刪除了C(1)唯一的強引用 v1,因此
... print(d[k1]) # WeakValueDictionary()里面 k1,v1 這個鍵值對消失了
...
>>> test_weak_value_dict()
<__main__.C object at 0x000001793E545198>
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 8, in test_weak_value_dict
File "C:\Users\ASUS\Anaconda3\lib\weakref.py", line 137, in __getitem__
o = self.data[key]()
KeyError: 'test1'
>>>
我們如果使用dict代替WeakValueDictionary(),會發生什么呢?
dict內部的引用都是強引用,因此我們刪除了C(1)的強引用v1,還有dict里面對C(1)的強引用,因此c(1)不會被銷毀
>>> import weakref
>>>
>>> class C:
... def __init__(self, value):
... self.value = value
...
>>> def test_weak_value_dict():
... d= dict()
... k1 = 'test1'
... v1 = C(1)
... d[k1] = v1
... print(d[k1])
... del v1
... print(d[k1])
...
>>> test_weak_value_dict()
<__main__.C object at 0x000001793E545390>
<__main__.C object at 0x000001793E545390>
可以看到沒有發生報錯,就是因為dict里面的引用為強引用
再看下面一個例子,直接報錯,搞明白原因了嗎?
>>> import weakref
>>>
>>> class C:
... def __init__(self, value):
... self.value = value
...
>>> def test_weak_value_dict():
... d= weakref.WeakValueDictionary()
... k1 = 'test1'
... d[k1] = C(1)
... print(d[k1])
...
>>> test_weak_value_dict()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 5, in test_weak_value_dict
File "C:\Users\ASUS\Anaconda3\lib\weakref.py", line 137, in __getitem__
o = self.data[key]()
KeyError: 'test1'
為什么報錯:keyerror?
因為這條記錄已經被刪除了。為什么?
我們看對C(1)的引用有誰? 只有WeakValueDictionary()中的弱引用,根本沒有強引用。
所以觸發垃圾回收機制將C(1)銷毀了,因此WeakValueDictionary()關於C(1)的記錄也就刪除了。
四、weakref.ref()
和weakref.proxy()
的區別
其實這兩個只是使用上有稍微的區別,proxy()
算是給用戶提供一個更加簡潔的接口,看下面的代碼就懂了。
>>>
>>> import weakref
>>> class C:
... def __init__(self, value):
... self.value = value
>>> c_obj = C(1)
>>> ref = weakref.ref(c_obj)
>>> ref()
<__main__.C object at 0x000001793E5454A8>
>>> ref().value
1
>>>
>>> proxy = weakref.proxy(c_obj)
>>> proxy
<weakproxy at 0x000001793DF55CC8 to C at 0x000001793E5454A8>
>>> proxy.value
1
我們在使用weak.ref
時,返回值ref,需要執行ref()
才是弱引用的對象,ref()
相當於 c_obj
而weakref.proxy
的返回值直接就是弱引用的對象,返回值proxy
直接相當於c_obj
————————————————
版權聲明:本文為CSDN博主「NeverLate_gogogo」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/NeverLate_gogogo/article/details/107021695