Python中的弱參照與基礎型別支援情況探究

2023-07-24 06:00:43

背景

最近有一個業務場景需要用Python自行實現一個簡單的LRU cache,不可避免的接觸到了弱參照這一概念,這裡記錄一下。

強參照

Python記憶體回收由垃圾回收器自動管理,當一個物件的參照計數歸0時,其記憶體就可能被回收掉,而參照計數器的數值其實就是代表有多少個強參照指向該物件,我們日常寫的Python程式碼如果沒有使用到weakref模組一般都只會涉及到強參照。
可以通過sys.getrefcount檢視物件的參照計數,如以下程式碼:

import sys

alist = [1, 2, 3] # alist參照計數=1
print(sys.getrefcount(alist)) # 包括getrefcount本身新增的強參照,輸出2
blist = alist
print(sys.getrefcount(alist)) # 新增blist強參照,輸出3
print(blist) # 輸出[1, 2, 3]
del blist
print(sys.getrefcount(alist)) # 刪除了blist,強參照-1, 輸出2

弱參照

與強參照相對,弱參照並不會影響物件的參照計數,也就是說其不影響物件是否被回收的判定,如以下程式碼:

import sys
import weakref

class tlist(list): # list本身不支援弱參照,但其子類支援
    pass

alist = tlist([1, 2, 3]) # alist參照計數=1
print(sys.getrefcount(alist)) # 輸出2
bref = weakref.ref(alist) # bref為對alist物件的弱參照
print(bref()) # 返回弱參照物件,輸出: [1, 2, 3]
print(sys.getrefcount(alist)) # 由於弱參照不影響參照計數,依然輸出2
del alist # 刪除alist,物件參照計數變為0
print(bref()) # 由於bref指向的物件已無任何強參照,返回None

如上程式碼所示弱參照不會影響物件的參照計數,亦即不會影響物件記憶體的回收,但是這裡碰到一個引人疑惑的點,就是Python中的基本資料型別對弱參照的支援分了三種情況。

基礎型別對於弱參照支援情況

基礎型別int、list、dict、tuple、str不支援弱參照,對其執行弱參照會報錯:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-9daeb515714d> in <module>
----> 1 weakref.ref(alist)

TypeError: cannot create weak reference to 'list' object

可以通過__weakrefoffset__檢視型別是否支援弱參照,該變數表示弱參照指標相對物件起始地址的偏移量,>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.

總結基礎型別對弱參照的支援分為以下三種情況(for python3.8):

  1. 對於list、dict、str本身不支援弱參照,但可以通過建立子類的方式對其進行弱參照
  2. 對於int、tuple本身及其子類均不支援弱參照
  3. set直接支援弱參照

這又是出於什麼考慮?通過一番探究得出以下可能原因:

  1. 絕大部分場景下,基礎型別使用並不涉及到弱參照,所以基礎型別不支援弱參照可以有效避免相應的overhead。
  2. 弱參照新增於Python2.1,所以對於之後新增的型別(包括object、type、set等)預設都是支援弱參照的,除非有明確的理由不這麼做。
  3. 對於list、dict、int、str、tuple這些2.1之前的基礎型別為了相容性考慮均預設不支援弱參照,而set新增與2.3,因此其直接支援弱參照。
  4. int、str、tuple這些不可變物件,在CPython直譯器中會有特殊的處理邏輯:
    4.1 如[-5, 256]範圍的小整數池一開始就被建立好了,在程式的整個生命週期無論是否被實際參照都不會被回收。
    4.2 又如對於同一個compilation unit的tuple物件,如果取值相同,編譯器會將獨立的多個相同的tuple物件處理為指向同一個物件的多個強參照。
    在這些情況下使用弱參照並沒有什麼明顯的好處,反而額外引入了overhead,綜合考慮直接對其不支援弱參照。
  5. 出於CPython的具體實現細節,對於int、tuple的子類也不支援弱參照。

轉載請註明出處,原文地址:https://www.cnblogs.com/AcAc-t/p/python_weakref_study.html

參考

https://docs.python.org/3.8/library/weakref.html
https://www.cnblogs.com/marsggbo/p/14831456.html
https://www.cnblogs.com/AcAc-t/p/python_weakref_study.html
https://stackoverflow.com/questions/52011430/python-which-types-support-weak-references