weakref — 弱引用

原始碼: Lib/weakref.py


weakref 模組允許 Python 程式設計師建立物件的 弱引用

在下文中,術語 referent 指的是被弱引用引用的物件。

對一個物件的弱引用不足以使該物件存活:當一個 referent 的唯一剩餘引用是弱引用時,垃圾回收 可以自由地銷燬該 referent 並將其記憶體用於其他用途。然而,在物件實際被銷燬之前,即使沒有強引用指向它,弱引用也可能返回該物件。

弱引用的主要用途是實現快取或對映,用於存放大型物件,其中希望大型物件不會僅僅因為出現在快取或對映中而存活。

例如,如果你有許多大型二進位制影像物件,你可能希望為每個物件關聯一個名稱。如果你使用 Python 字典將名稱對映到影像,或將影像對映到名稱,那麼影像物件將僅僅因為它們作為值或鍵出現在字典中而保持存活。weakref 模組提供的 WeakKeyDictionaryWeakValueDictionary 類是替代方案,它們使用弱引用來構造對映,而不會僅僅因為物件出現在對映物件中就使其存活。例如,如果一個影像物件是 WeakValueDictionary 中的一個值,那麼當對該影像物件的最後剩餘引用是弱對映持有的弱引用時,垃圾回收可以回收該物件,並且它在弱對映中的相應條目也會被刪除。

WeakKeyDictionaryWeakValueDictionary 在其實現中使用弱引用,為弱引用設定回撥函式,當鍵或值被垃圾回收時通知弱字典。WeakSet 實現了 set 介面,但對其元素保持弱引用,就像 WeakKeyDictionary 所做的那樣。

finalize 提供了一種直接的方法來註冊一個清理函式,該函式將在物件被垃圾回收時呼叫。這比在原始弱引用上設定回撥函式更簡單,因為模組會自動確保 finalizer 在物件被回收之前保持存活。

大多數程式會發現使用這些弱容器型別之一或 finalize 就足夠了——通常沒有必要直接建立自己的弱引用。weakref 模組公開了底層機制,以滿足高階用途的需求。

並非所有物件都可以被弱引用。支援弱引用的物件包括類例項、用 Python 編寫的函式(而非 C)、例項方法、集合、不可變集合、一些檔案物件生成器、型別物件、套接字、陣列、雙端佇列、正則表示式模式物件和程式碼物件。

版本 3.2 中有修改: 增加了對 thread.lock、threading.Lock 和程式碼物件的支援。

一些內建型別,例如 listdict,不直接支援弱引用,但可以透過子類化新增支援。

class Dict(dict):
    pass

obj = Dict(red=1, green=2, blue=3)   # this object is weak referenceable

CPython 實現細節: 其他內建型別,例如 tupleint,即使被子類化也不支援弱引用。

擴充套件型別可以很容易地支援弱引用;請參閱弱引用支援

當為給定型別定義了 __slots__ 時,除非在 __slots__ 宣告的字串序列中也存在字串 '__weakref__',否則弱引用支援將被停用。有關詳細資訊,請參閱__slots__ 文件

class weakref.ref(object[, callback])

返回對 object 的弱引用。如果 referent 仍然存活,可以透過呼叫引用物件來檢索原始物件;如果 referent 不再存活,呼叫引用物件將導致返回 None。如果提供了 callback 且不為 None,並且返回的弱引用物件仍然存活,則當物件即將被終結時,將呼叫 callback;弱引用物件將作為唯一引數傳遞給 callback;此時 referent 將不再可用。

允許為同一個物件構造多個弱引用。為每個弱引用註冊的回撥將按照從最近註冊到最舊註冊的順序呼叫。

回撥引發的異常將在標準錯誤輸出中註明,但無法傳播;它們的處理方式與物件 __del__() 方法引發的異常完全相同。

如果 object可雜湊的,則弱引用也是可雜湊的。即使 object 被刪除,它們也將保持其雜湊值。如果僅在 object 被刪除後第一次呼叫 hash(),則呼叫將引發 TypeError

弱引用支援相等性測試,但不支援排序。如果 referent 仍然存活,則兩個引用與其 referent 具有相同的相等關係(無論 callback 如何)。如果任一 referent 已被刪除,則僅當引用物件是同一個物件時,引用才相等。

這是一個可子類化的型別,而不是一個工廠函式。

__callback__

此只讀屬性返回當前與弱引用關聯的回撥。如果沒有回撥,或者弱引用的 referent 不再存活,則此屬性的值將為 None

版本 3.4 中有修改: 添加了 __callback__ 屬性。

weakref.proxy(object[, callback])

返回一個對 object 的代理,該代理使用弱引用。這支援在大多數上下文中使用代理,而無需像弱引用物件那樣進行顯式解引用。返回物件的型別將是 ProxyTypeCallableProxyType,具體取決於 object 是否可呼叫。無論 referent 如何,代理物件都不可 雜湊;這避免了許多與其根本可變性相關的深層問題,並阻止它們用作字典鍵。callbackref() 函式同名的引數相同。

在 referent 被垃圾回收後訪問代理物件的屬性會引發 ReferenceError

版本 3.8 中有修改: 擴充套件了對代理物件的運算子支援,包括矩陣乘法運算子 @@=

weakref.getweakrefcount(object)

返回引用 object 的弱引用和代理的數量。

weakref.getweakrefs(object)

返回引用 object 的所有弱引用和代理物件的列表。

class weakref.WeakKeyDictionary([dict])

弱引用鍵的對映類。當不再有對鍵的強引用時,字典中的條目將被丟棄。這可以用於將額外資料與應用程式其他部分擁有的物件關聯起來,而無需向這些物件新增屬性。這對於覆蓋屬性訪問的物件特別有用。

請注意,當一個與現有鍵值相等(但標識不等)的鍵被插入到字典中時,它會替換值,但不會替換現有鍵。因此,當對原始鍵的引用被刪除時,它也會刪除字典中的條目。

>>> class T(str): pass
...
>>> k1, k2 = T(), T()
>>> d = weakref.WeakKeyDictionary()
>>> d[k1] = 1   # d = {k1: 1}
>>> d[k2] = 2   # d = {k1: 2}
>>> del k1      # d = {}

一種解決方法是在重新賦值之前刪除鍵。

>>> class T(str): pass
...
>>> k1, k2 = T(), T()
>>> d = weakref.WeakKeyDictionary()
>>> d[k1] = 1   # d = {k1: 1}
>>> del d[k1]
>>> d[k2] = 2   # d = {k2: 2}
>>> del k1      # d = {k2: 2}

版本 3.9 中有修改: 增加了對 ||= 運算子的支援,如 PEP 584 中所述。

WeakKeyDictionary 物件有一個額外的暴露內部引用的方法。這些引用在使用時不能保證是“活的”,因此在呼叫和使用引用之前需要進行檢查。這可以用於避免建立導致垃圾回收器比必要時間更長地保留鍵的引用。

WeakKeyDictionary.keyrefs()

返回對鍵的弱引用的可迭代物件。

class weakref.WeakValueDictionary([dict])

弱引用值的對映類。當不再存在對值的強引用時,字典中的條目將被丟棄。

版本 3.9 中有修改: 增加了對 ||= 運算子的支援,如 PEP 584 中所述。

WeakValueDictionary 物件有一個額外的方法,與 WeakKeyDictionary.keyrefs() 方法具有相同的問題。

WeakValueDictionary.valuerefs()

返回對值的弱引用的可迭代物件。

class weakref.WeakSet([elements])

集合類,對其元素保持弱引用。當不再存在對元素的強引用時,元素將被丟棄。

class weakref.WeakMethod(method[, callback])

一個自定義的 ref 子類,它模擬對繫結方法(即,在類上定義並在例項上查詢的方法)的弱引用。由於繫結方法是瞬態的,標準弱引用無法保持對其的引用。WeakMethod 具有特殊的程式碼,可以在物件或原始函式消亡之前重新建立繫結方法。

>>> class C:
...     def method(self):
...         print("method called!")
...
>>> c = C()
>>> r = weakref.ref(c.method)
>>> r()
>>> r = weakref.WeakMethod(c.method)
>>> r()
<bound method C.method of <__main__.C object at 0x7fc859830220>>
>>> r()()
method called!
>>> del c
>>> gc.collect()
0
>>> r()
>>>

callbackref() 函式同名的引數相同。

在 3.4 版本加入。

class weakref.finalize(obj, func, /, *args, **kwargs)

返回一個可呼叫的終結器物件,當 obj 被垃圾回收時,它將被呼叫。與普通弱引用不同,終結器將始終存活直到引用物件被回收,這極大地簡化了生命週期管理。

終結器在被呼叫(顯式呼叫或垃圾回收時呼叫)之前被認為是 活著的,之後則是 死的。呼叫一個活著的終結器會返回 func(*arg, **kwargs) 的評估結果,而呼叫一個已死的終結器則返回 None

在垃圾回收過程中,終結器回撥引發的異常將顯示在標準錯誤輸出中,但無法傳播。它們的處理方式與物件 __del__() 方法或弱引用回撥引發的異常相同。

當程式退出時,除非其 atexit 屬性已設定為 false,否則每個剩餘的活動終結器都會被呼叫。它們以建立的相反順序呼叫。

直譯器關閉的後期,當模組全域性變數可能已被 None 替換時,終結器絕不會呼叫其回撥。

__call__()

如果 self 處於活動狀態,則將其標記為已死亡並返回呼叫 func(*args, **kwargs) 的結果。如果 self 處於已死亡狀態,則返回 None

detach()

如果 self 處於活動狀態,則將其標記為已死亡並返回元組 (obj, func, args, kwargs)。如果 self 處於已死亡狀態,則返回 None

peek()

如果 self 處於活動狀態,則返回元組 (obj, func, args, kwargs)。如果 self 處於已死亡狀態,則返回 None

alive

屬性,如果終結器處於活動狀態則為 True,否則為 False。

atexit

一個可寫的布林屬性,預設為 True。當程式退出時,它會呼叫所有剩餘的活動終結器,其中 atexit 為 True。它們以建立的相反順序呼叫。

備註

重要的是要確保 funcargskwargs 不直接或間接擁有對 obj 的任何引用,否則 obj 將永遠不會被垃圾回收。特別地,func 不應是 obj 的繫結方法。

在 3.4 版本加入。

weakref.ReferenceType

弱引用物件的型別物件。

weakref.ProxyType

不可呼叫物件的代理型別物件。

weakref.CallableProxyType

可呼叫物件的代理型別物件。

weakref.ProxyTypes

包含所有代理型別物件的序列。這可以簡化測試物件是否為代理而無需依賴於命名兩種代理型別。

參見

PEP 205 - 弱引用

此功能的提案和原理,包括早期實現連結和有關其他語言中類似功能的資訊。

弱引用物件

弱引用物件除了 ref.__callback__ 之外沒有其他方法和屬性。弱引用物件允許透過呼叫它來獲取被引用物件(如果它仍然存在)。

>>> import weakref
>>> class Object:
...     pass
...
>>> o = Object()
>>> r = weakref.ref(o)
>>> o2 = r()
>>> o is o2
True

如果被引用物件不再存在,呼叫引用物件將返回 None

>>> del o, o2
>>> print(r())
None

測試弱引用物件是否仍然存活應該使用表示式 ref() is not None。通常,需要使用引用物件的應用程式程式碼應該遵循這種模式。

# r is a weak reference object
o = r()
if o is None:
    # referent has been garbage collected
    print("Object has been deallocated; can't frobnicate.")
else:
    print("Object is still live!")
    o.do_something_useful()

使用單獨的“存活”測試會在多執行緒應用程式中建立競爭條件;另一個執行緒可能在弱引用被呼叫之前導致其失效;上面所示的習慣用法在多執行緒和單執行緒應用程式中都是安全的。

可以透過子類化建立 ref 物件的專用版本。這在 WeakValueDictionary 的實現中使用,以減少對映中每個條目的記憶體開銷。這可能最有助於將額外資訊與引用關聯起來,但也可以用於在檢索被引用物件時插入額外的處理。

這個簡單的例子展示了 ref 的子類如何用於儲存有關物件的額外資訊,並影響在訪問被引用物件時返回的值。

import weakref

class ExtendedRef(weakref.ref):
    def __init__(self, ob, callback=None, /, **annotations):
        super().__init__(ob, callback)
        self.__counter = 0
        for k, v in annotations.items():
            setattr(self, k, v)

    def __call__(self):
        """Return a pair containing the referent and the number of
        times the reference has been called.
        """
        ob = super().__call__()
        if ob is not None:
            self.__counter += 1
            ob = (ob, self.__counter)
        return ob

示例

這個簡單的示例展示了應用程式如何使用物件 ID 來檢索它以前見過的物件。然後,可以在其他資料結構中使用物件的 ID,而不會強制物件保持存活,但如果它們存活,仍然可以透過 ID 檢索這些物件。

import weakref

_id2obj_dict = weakref.WeakValueDictionary()

def remember(obj):
    oid = id(obj)
    _id2obj_dict[oid] = obj
    return oid

def id2obj(oid):
    return _id2obj_dict[oid]

終結器物件

使用 finalize 的主要好處是,它使得註冊回撥函式變得簡單,而無需保留返回的終結器物件。例如:

>>> import weakref
>>> class Object:
...     pass
...
>>> kenny = Object()
>>> weakref.finalize(kenny, print, "You killed Kenny!")
<finalize object at ...; for 'Object' at ...>
>>> del kenny
You killed Kenny!

終結器也可以直接呼叫。然而,終結器最多隻會呼叫一次回撥函式。

>>> def callback(x, y, z):
...     print("CALLBACK")
...     return x + y + z
...
>>> obj = Object()
>>> f = weakref.finalize(obj, callback, 1, 2, z=3)
>>> assert f.alive
>>> assert f() == 6
CALLBACK
>>> assert not f.alive
>>> f()                     # callback not called because finalizer dead
>>> del obj                 # callback not called because finalizer dead

您可以使用其 detach() 方法登出終結器。這會終止終結器並返回在建立時傳遞給建構函式的引數。

>>> obj = Object()
>>> f = weakref.finalize(obj, callback, 1, 2, z=3)
>>> f.detach()
(<...Object object ...>, <function callback ...>, (1, 2), {'z': 3})
>>> newobj, func, args, kwargs = _
>>> assert not f.alive
>>> assert newobj is obj
>>> assert func(*args, **kwargs) == 6
CALLBACK

除非你將 atexit 屬性設定為 False,否則如果終結器在程式退出時仍然存活,它將被呼叫。例如:

>>> obj = Object()
>>> weakref.finalize(obj, print, "obj dead or exiting")
<finalize object at ...; for 'Object' at ...>
>>> exit()
obj dead or exiting

將終結器與 __del__() 方法進行比較

假設我們想建立一個類,其例項代表臨時目錄。當發生以下事件中的第一個時,目錄及其內容應被刪除:

  • 物件被垃圾回收,

  • 呼叫了物件的 remove() 方法,或

  • 程式退出。

我們可能會嘗試使用 __del__() 方法實現該類,如下所示:

class TempDir:
    def __init__(self):
        self.name = tempfile.mkdtemp()

    def remove(self):
        if self.name is not None:
            shutil.rmtree(self.name)
            self.name = None

    @property
    def removed(self):
        return self.name is None

    def __del__(self):
        self.remove()

從 Python 3.4 開始,__del__() 方法不再阻止引用迴圈被垃圾回收,並且在直譯器關閉期間模組全域性變數不再強制設定為 None。因此,這段程式碼在 CPython 上應該沒有任何問題。

然而,__del__() 方法的處理方式因實現而異,因為它取決於直譯器垃圾回收器實現的內部細節。

一個更健壯的替代方案是定義一個僅引用其所需特定函式和物件的終結器,而不是訪問物件的完整狀態:

class TempDir:
    def __init__(self):
        self.name = tempfile.mkdtemp()
        self._finalizer = weakref.finalize(self, shutil.rmtree, self.name)

    def remove(self):
        self._finalizer()

    @property
    def removed(self):
        return not self._finalizer.alive

這樣定義後,我們的終結器只接收它清理目錄所需的詳細資訊。如果物件從未被垃圾回收,終結器仍會在退出時被呼叫。

基於弱引用的終結器的另一個優點是它們可以用於為第三方控制定義的類註冊終結器,例如在模組解除安裝時執行程式碼:

import weakref, sys
def unloading_module():
    # implicit reference to the module globals from the function body
weakref.finalize(sys.modules[__name__], unloading_module)

備註

如果你在程式退出時在一個守護執行緒中建立了一個終結器物件,那麼終結器可能不會在退出時被呼叫。然而,在守護執行緒中,atexit.register()try: ... finally: ...with: ... 也不能保證清理會發生。