Python weakref (弱引用 ) 教程


原文: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]這個對象添加了一個標簽。當然,這種說法是只限於對可變對象的操作。我們可以參考下圖:

image

每個變量都有 標識 、類型 和 值。對象一旦創建,它的標識絕不會變;可以把標識理解為對象在內存中的地址。is 運算符比較兩個對象的標識;id() 函數返回對象標識的整數表示。

因此當我們對上面執行 id(my_list1)  與 id(my_list) 他們返回的值是一樣的,因為他們指向同一個對象,my_listmy_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_listmy_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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM