背景
最近有一個(gè)業(yè)務(wù)場(chǎng)景需要用Python自行實(shí)現(xiàn)一個(gè)簡(jiǎn)單的LRU cache,不可避免的接觸到了弱引用這一概念,這里記錄一下。
強(qiáng)引用
Python內(nèi)存回收由垃圾回收器自動(dòng)管理,當(dāng)一個(gè)對(duì)象的引用計(jì)數(shù)歸0時(shí),其內(nèi)存就可能被回收掉,而引用計(jì)數(shù)器的數(shù)值其實(shí)就是代表有多少個(gè)強(qiáng)引用指向該對(duì)象,我們?nèi)粘懙腜ython代碼如果沒有使用到weakref模塊一般都只會(huì)涉及到強(qiáng)引用。
可以通過(guò)sys.getrefcount查看對(duì)象的引用計(jì)數(shù),如以下代碼:
import sys
alist = [1, 2, 3] # alist引用計(jì)數(shù)=1
print(sys.getrefcount(alist)) # 包括getrefcount本身新增的強(qiáng)引用,輸出2
blist = alist
print(sys.getrefcount(alist)) # 新增blist強(qiáng)引用,輸出3
print(blist) # 輸出[1, 2, 3]
del blist
print(sys.getrefcount(alist)) # 刪除了blist,強(qiáng)引用-1, 輸出2
弱引用
與強(qiáng)引用相對(duì),弱引用并不會(huì)影響對(duì)象的引用計(jì)數(shù),也就是說(shuō)其不影響對(duì)象是否被回收的判定,如以下代碼:
import sys
import weakref
class tlist(list): # list本身不支持弱引用,但其子類支持
pass
alist = tlist([1, 2, 3]) # alist引用計(jì)數(shù)=1
print(sys.getrefcount(alist)) # 輸出2
bref = weakref.ref(alist) # bref為對(duì)alist對(duì)象的弱引用
print(bref()) # 返回弱引用對(duì)象,輸出: [1, 2, 3]
print(sys.getrefcount(alist)) # 由于弱引用不影響引用計(jì)數(shù),依然輸出2
del alist # 刪除alist,對(duì)象引用計(jì)數(shù)變?yōu)?
print(bref()) # 由于bref指向的對(duì)象已無(wú)任何強(qiáng)引用,返回None
如上代碼所示弱引用不會(huì)影響對(duì)象的引用計(jì)數(shù),亦即不會(huì)影響對(duì)象內(nèi)存的回收,但是這里碰到一個(gè)引人疑惑的點(diǎn),就是Python中的基本數(shù)據(jù)類型對(duì)弱引用的支持分了三種情況。
基礎(chǔ)類型對(duì)于弱引用支持情況
基礎(chǔ)類型int、list、dict、tuple、str不支持弱引用,對(duì)其執(zhí)行弱引用會(huì)報(bào)錯(cuò):
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-3-9daeb515714d> in <module>
----> 1 weakref.ref(alist)
TypeError: cannot create weak reference to 'list' object
可以通過(guò)__weakrefoffset__查看類型是否支持弱引用,該變量表示弱引用指針相對(duì)對(duì)象起始地址的偏移量,>0表示支持弱引用:
In [1]: int.__weakrefoffset__
Out[1]: 0
In [2]: str.__weakrefoffset__
Out[2]: 0
In [3]: tuple.__weakrefoffset__
Out[3]: 0
In [4]: list.__weakrefoffset__
Out[4]: 0
In [5]: dict.__weakrefoffset__
Out[5]: 0
In [6]: set.__weakrefoffset__
Out[6]: 192
官方文檔中介紹:
Several built-in types such as list and dict do not directly support weak references but can add support through subclassing:
CPython implementation detail: Other built-in types such as tuple and int do not support weak references even when subclassed.
總結(jié)基礎(chǔ)類型對(duì)弱引用的支持分為以下三種情況(for python3.8):
1、對(duì)于list、dict、str本身不支持弱引用,但可以通過(guò)創(chuàng)建子類的方式對(duì)其進(jìn)行弱引用
2、對(duì)于int、tuple本身及其子類均不支持弱引用
3、set直接支持弱引用
這又是出于什么考慮?通過(guò)一番探究得出以下可能原因:
1、絕大部分場(chǎng)景下,基礎(chǔ)類型使用并不涉及到弱引用,所以基礎(chǔ)類型不支持弱引用可以有效避免相應(yīng)的overhead。
2、弱引用添加于Python2.1,所以對(duì)于之后添加的類型(包括object、type、set等)默認(rèn)都是支持弱引用的,除非有明確的理由不這么做。
3、對(duì)于list、dict、int、str、tuple這些2.1之前的基礎(chǔ)類型為了兼容性考慮均默認(rèn)不支持弱引用,而set添加與2.3,因此其直接支持弱引用。
4、int、str、tuple這些不可變對(duì)象,在CPython解釋器中會(huì)有特殊的處理邏輯:
4.1 如[-5, 256]范圍的小整數(shù)池一開始就被創(chuàng)建好了,在程序的整個(gè)生命周期無(wú)論是否被實(shí)際引用都不會(huì)被回收。
4.2 又如對(duì)于同一個(gè)compilation unit的tuple對(duì)象,如果取值相同,編譯器會(huì)將獨(dú)立的多個(gè)相同的tuple對(duì)象處理為指向同一個(gè)對(duì)象的多個(gè)強(qiáng)引用。
在這些情況下使用弱引用并沒有什么明顯的好處,反而額外引入了overhead,綜合考慮直接對(duì)其不支持弱引用。
5、出于CPython的具體實(shí)現(xiàn)細(xì)節(jié),對(duì)于int、tuple的子類也不支持弱引用。