weakref --- 弱參照

原始碼:Lib/weakref.py


weakref 模組允許 Python 程式設計師建立對物件的 弱參照

在以下文章中,術語 參照目標 (referent) 表示被弱參照所參考的物件。

對物件的弱參照不足以使物件保持存在:當對參照目標的唯一剩下的參照是弱參照時,garbage collection 可以自由地銷毀參照目標並將其記憶體重新用於其他用途。然而,在物件被確實銷毀之前,即使沒有對該物件的強參照 (strong reference),弱參照也可能會回傳該物件。

弱參照的主要用途是實作保存大型物件的快取或對映,其不希望大型物件僅僅因為它出現在快取或對映中而保持存在。

例如,如果你有許多大型的二進位影像物件,你可能會想要為每個物件關聯 (associate) 一個名稱。如果你使用 Python 字典將名稱對映到影像,或將影像對映到名稱,則影像物件將保持存活,僅因為它們在字典中作為值 (value) 或鍵 (key) 出現。weakref 模組提供的 WeakKeyDictionaryWeakValueDictionary 類別是另一種選擇,它們使用弱參照來建構對映,這些對映不會僅因為物件出現在對映物件中而使物件保持存活。例如,如果一個影像物件是 WeakValueDictionary 中的一個值,那麼當對該影像物件最後的參照是弱對映 (weak mapping) 所持有的弱參照時,垃圾回收 (garbage collection) 可以回收該物件,且其對應的條目在弱對映中會被完全地刪除。

WeakKeyDictionaryWeakValueDictionary 在其實作中使用弱參照,在弱參照上設定回呼函式,此弱參照在垃圾回收取回鍵或值時通知弱字典。WeakSet 實作了 set 介面,但保留對其元素的弱參照,就像 WeakKeyDictionary 一樣。

finalize 提供了一種直接的方法來註冊在物件被垃圾回收時呼叫的清理函式。這比在原始弱參照上設定回呼函式更容易使用,因為模組在物件被回收前會自動確保最終化器 (finalizer) 保持存活。

大多數程式應該發現使用這些弱容器種類之一或 finalize 就足夠了—通常不需要直接建立自己的弱參照。低層級的機制由 weakref 模組公開,以利於進階用途。

並非所有物件都可以被弱參照。支援弱參照的物件包括類別實例、用 Python (但不是C)編寫的函式、實例方法、集合、凍結集合 (frozenset)、一些檔案物件產生器、類型物件、socket、陣列、雙向佇列、正規表示式模式物件和程式碼物件。

在 3.2 版的變更: 新增了對 thread.lock、threading.Lock 和程式碼物件的支援。

一些內建型別,例如 listdict 不直接支援弱參照,但可以透過子類別化來支援:

class Dict(dict):
    pass

obj = Dict(red=1, green=2, blue=3)   # 這個物件是可被弱參照的

CPython 實作細節: 其他內建型別,例如 tupleint 即使在子類別化時也不支援弱參照。

擴充型別 (extension type) 可以輕易地支援弱參照;請參閱 Weak Reference Support

當為給定的型別定義 __slots__ 時,弱參照支援將被停用,除非 '__weakref__' 字串也存在於 __slots__ 宣告的字串序列中。詳情請參閱 __slots__ 文件

class weakref.ref(object[, callback])

回傳對 object 的弱參照。如果參照目標仍存活,則可以透過呼叫參照物件來取回原始物件;如果參照目標已不存活,呼叫參照物件將導致 None 被回傳。如果 callback 被提供而非 None,且回傳的弱參照物件仍存活,那麼當物件即將被最終化 (finalize) 時,回呼將被呼叫;弱參照物件將作為唯一的參數傳遞給回呼;參照物件將不再可用。

為同一個物件建構多個弱參照是可行的。為每個弱參照註冊的回呼將按照最新到最舊註冊的回呼順序來被呼叫。

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

如果 object可雜湊的,那麼弱參照就是可雜湊的。即使在 object 被刪除後,它們仍將保留其雜湊值。如果僅在 object 刪除後才第一次呼叫 hash(),則該呼叫將引發 TypeError

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

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

__callback__

此唯讀屬性回傳目前與弱參照關聯的回呼。如果沒有回呼或弱參照的參照目標已不存活,那麼該屬性的值為 None

在 3.4 版的變更: 新增 __callback__ 屬性。

weakref.proxy(object[, callback])

回傳一個使用弱參照的 object 的代理 (proxy)。這支援在大多數情境中使用代理,而不需要對弱參照物件明確地取消參照。回傳的物件將具有 ProxyTypeCallableProxyType 型別,具體取決於 object 是否為可呼叫物件。無論參照目標如何,代理物件都不是 hashable;這避免了與其基本可變物件本質相關的許多問題,並阻止它們作為字典的鍵被使用。callbackref() 函式的同名參數是相同的。

在參照目標被垃圾回收後存取代理物件的屬性會引發 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 = {}

一個變通的解法是在重新賦值 (reassignment) 之前刪除鍵:

>>> 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 子類別,其模擬對繫結方法 (bound method) (即在類別上定義並在實例上查找的方法)的弱參照。由於繫結方法是短暫存在的,因此標準弱參照無法保留它。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,否則每個存活的最終化器會被呼叫。它們以與建立相反的順序被呼叫。

當模組的 globals 可能被 None 取代時,最終化器永遠不會在 interpreter shutdown 的後期調用(invoke)其回呼。

__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()

使用對「活性 (liveness)」的單獨測試會在執行緒應用程式中建立競爭條件 (race condition);另一個執行緒可能在弱參照被呼叫之前讓該弱參照失效;上方顯示的慣用作法在執行緒應用程式和單執行緒應用程式中都是安全的。

可以透過子類別化來建立 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__() 方法不再阻止參照循環 (reference cycle) 被垃圾回收,並且在 interpreter shutdown 期間不再強制將模組的 globals 設為 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

定義如下,我們的最終化器僅接收對適當清理目錄所需的詳細資訊的參照。如果物件從未被垃圾回收,則最終化器仍將在結束時被呼叫。

基於 weakref 的最終化器的另一個優點是它們可用於為定義由第三方控制的類別註冊最終化器,例如在卸載模組時執行程式碼:

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

備註

如果在程式結束時在常駐的 (daemonic) 執行緒中建立最終化器物件,則最終化器有可能在結束時不會被呼叫。然而,在常駐的執行緒中 atexit.register()try: ... finally: ...with: ... 也不保證清理會發生。