Following system colour scheme - Python 增強提案 Selected dark colour scheme - Python 增強提案 Selected light colour scheme - Python 增強提案

Python 增強提案

PEP 205 – 弱引用

作者:
Fred L. Drake, Jr. <fred at fdrake.net>
狀態:
最終版
型別:
標準跟蹤
建立日期:
2000年7月14日
Python 版本:
2.1
釋出歷史:
2001年1月11日

目錄

動機

Python程式設計師已經注意到弱引用的兩個基本應用:物件快取和減少迴圈引用帶來的麻煩。

快取(弱字典)

需要維護代表外部狀態的物件,將單個例項對映到外部現實,其中允許多個例項對映到相同的外部資源會給維護例項間的同步帶來不必要的困難。在這些情況下,一種常見的慣用方法是支援例項快取;工廠函式用於返回新例項或現有例項。

這種方法的困難在於必須容忍兩種情況之一:要麼快取無限制地增長,要麼需要在應用程式的其他地方進行快取的顯式管理。後者可能非常繁瑣,並且會導致比實際解決當前問題所需的更多程式碼,而前者對於長時間執行的程序甚至記憶體需求較大的相對較短的程序來說是不可接受的。

  • 無論內部使用者有多少,需要由單個例項表示的外部物件。這對於表示需要整體寫回磁碟而不是每次使用時鎖定和修改的檔案可能很有用。
  • 建立成本高昂但可能被多個內部消費者需要的物件。類似於第一種情況,但不一定與外部資源繫結,並且可能不是共享狀態的問題。只有當存在某種“軟”引用或單個物件的使用者生命週期高度重疊時,弱引用才在這種情況下有用。

迴圈引用

  • DOM需要大量的迴圈引用(到父節點和文件節點),但這些可以透過使用從每個節點到其父節點的弱字典對映來消除。這在諸如xml.dom.pulldom之類的上下文中可能特別有用,允許.unlink()操作變為無操作。

本提案分為以下幾節

  • 提議的解決方案
  • 實現策略
  • 可能的應用
  • Python中之前的弱引用工作
  • Java中的弱引用

由於早期提案的全文似乎無法在網上找到,因此將其作為附錄包含在內。

解決方案空間的各個方面

弱引用問題有兩個不同的方面

  • 弱引用失效
  • 弱引用向Python程式碼的呈現

失效

過去解決弱引用失效的方法通常依賴於儲存一個強引用,並能夠檢查弱引用物件的所有例項,並在其引用物件的引用計數變為一(表明弱引用儲存的引用是最後一個剩餘引用)時使它們失效。這樣做的好處是Python的記憶體管理機制無需改變,並且任何型別都可以被弱引用。

這種失效方法的缺點是它假設弱引用的管理被足夠頻繁地呼叫,以便在相當短的時間內發現弱引用物件;由於這意味著掃描某個資料結構以使引用失效,這是一個O(N)的操作(N是弱引用物件的數量),因此對於任何單個弱引用物件而言,這種操作並不能有效地分攤成本。這還假設應用程式以某種頻率呼叫處理弱引用物件的程式碼,這使得弱引用對庫程式碼的吸引力降低。

另一種失效方法是讓解除分配程式碼意識到弱引用的可能性,並在每次物件解除分配時專門呼叫弱引用管理程式碼來處理失效。這要求更改可弱引用物件的tp_dealloc處理程式;對於支援弱引用的物件,需要在處理程式的“頂部”進行額外的呼叫,並且還需要一種將物件對映到該物件的弱引用鏈的有效方法。

呈現方式

弱引用向Python層呈現的兩種方式是:作為顯式引用物件,需要對其執行某些操作才能檢索對底層物件的可用引用;以及作為儘可能偽裝成原始物件的代理物件。

當在Python中新增額外的物件管理層時,引用物件很容易使用;可以顯式檢查引用的活躍性,而無需在引用物件上呼叫操作並捕獲在使用無效弱引用時引發的特殊異常。

然而,許多使用者偏愛代理方法,僅僅因為弱引用看起來非常像原始物件。

提議的解決方案

弱引用應該能夠指向任何可能具有大量記憶體大小(直接或間接)或持有對外部資源(資料庫連線、開啟檔案等)引用的Python物件。

一個新模組 weakref 將包含用於建立弱引用的新函式。weakref.ref() 將建立一個“弱引用物件”,並可選地附加一個回撥,該回調將在物件即將結束時被呼叫。weakref.mapping() 將建立一個“弱字典”。第三個函式 weakref.proxy() 將建立一個行為與原始物件有些相似的代理物件。

弱引用物件將允許在引用物件未被回收時訪問它,並確定該物件是否仍存在於記憶體中。透過呼叫引用物件來檢索引用物件。如果引用物件不再存在,則將返回 None。

弱字典將任意鍵對映到值,但不擁有對值的引用。當值被終止時,作為其值的 (鍵, 值) 對將從包含此類對的所有對映中刪除。與字典一樣,弱字典不可雜湊。

代理物件是弱引用,它們儘可能地嘗試表現得像它們所代理的物件。無論底層型別如何,代理都是不可雜湊的,因為它們作為弱引用起作用的能力依賴於基本的可變性,這會導致在用作字典鍵時失敗——即使在引用物件死亡之前計算出正確的雜湊值,生成的代理也無法用作字典鍵,因為它在引用物件過期後無法比較,而可比較性對於字典鍵是必需的。在引用物件死亡後對代理物件執行操作,在大多數情況下會導致 weakref.ReferenceError 被引發。“is”比較、type()id() 將繼續工作,但始終指向代理而非引用物件。

註冊到弱引用的回撥必須接受一個引數,該引數將是弱引用或代理物件本身。在回撥中不能訪問或復活該物件。

實現策略

弱引用的實現將包含一個引用容器列表,必須為每個可弱引用物件清除這些容器。如果引用來自弱字典,則首先清除字典條目。然後,將任何相關的回撥與作為引數傳遞的物件一起呼叫。一旦所有回撥都被呼叫,物件將被終結和解除分配。

許多內建型別將參與弱引用管理,並且任何擴充套件型別都可以選擇參與。型別結構將包含一個附加欄位,該欄位提供例項結構中的偏移量,其中包含弱引用結構的列表。如果該欄位的值 <= 0,則物件不參與。在這種情況下,weakref.ref()<weakdict>.__setitem__().setdefault() 以及項賦值將引發 TypeError。如果該欄位的值 > 0,則可以生成新的弱引用並將其新增到列表中。

採用這種方法是為了允許任意擴充套件型別參與,而不會對數字或其他小型型別造成記憶體開銷。

支援弱引用的標準型別包括例項、函式以及繫結和非繫結方法。隨著 Python 2.2 中引入類型別(“新式類”),型別也開始支援弱引用。如果類型別的例項具有可弱引用的基型別,或者類未指定 __slots__,或者有一個名為 __weakref__ 的槽,則它們是可弱引用的。生成器也支援弱引用。

可能的應用

PyGTK+ 繫結?

Tkinter——可以透過使用從控制元件到其父級的弱引用來避免迴圈引用。在典型情況下,物件不會更早地被丟棄,但程式設計師在釋放引用之前呼叫 .destroy() 的依賴性會大大降低。這主要有利於長時間執行的應用程式。

DOM 樹。

Python中之前的弱引用工作

Dianne Hackborn 提出了一個名為“虛擬引用”的東西。‘vref’物件與 java.lang.ref.WeakReference 物件非常相似,只是沒有等效的失效佇列。實現“弱字典”將像在 Java 中僅使用弱引用(沒有失效佇列)一樣困難。有關此資訊已從網路上消失,但作為附錄包含在下方。

Marc-André Lemburg 的 mx.Proxy 包

Dieter Maurer 的 weakdict 模組是用 C 和 Python 實現的。似乎網頁自 Python 1.5.2a 以來就沒有更新過,所以我還不確定該實現是否與 Python 2.0 相容。

Alex Shindich 的 PyWeakReference

Eric Tiedemann 有一個弱字典實現

Java中的弱引用

http://java.sun.com/j2se/1.3/docs/api/java/lang/ref/package-summary.html

Java 提供了三種形式的弱引用和一種有趣的輔助類。這三種形式被稱為“弱”、“軟”和“虛”引用。相關的類定義在 java.lang.ref 包中。

對於每種引用型別,可以選擇在記憶體分配器使其失效時將引用新增到佇列中。此功能的主要目的似乎是允許構成更大的結構以包含弱引用語義,而無需施加大量的額外鎖定要求。例如,使用此功能建立“弱”雜湊表並不困難,該雜湊表在引用不再被其他地方使用時刪除鍵和引用物件。對於不帶某種失效通知佇列的物件使用弱引用會導致雜湊表上所需的各種操作的實現更加繁瑣。如果儲存物件的解除分配不頻繁,這可能會成為效能瓶頸。

Java 的“弱”引用最像 Dianne Hackborn 早期的 vref 提案:一個引用物件指向一個 Python 物件,但不擁有該物件的引用。當該物件被解除分配時,引用物件失效。引用物件的使用者可以輕鬆確定引用已失效,或者在嘗試使用被引用物件時會引發 NullObjectDereferenceError。

“軟”引用與此類似,但不會在所有其他對所引用物件的引用被釋放後立即失效。“軟”引用確實擁有一個引用,但允許記憶體分配器在記憶體被其他地方需要時釋放所引用物件。目前尚不清楚這是否意味著軟引用在 malloc() 實現呼叫 sbrk() 或其等效項之前被釋放,或者軟引用是否僅在 malloc() 返回 NULL 時才被清除。

“虛”引用略有不同;與弱引用和軟引用不同,當引用被新增到其佇列時,引用物件不會被清除。當一個物件的所有虛引用都出隊時,該物件將被清除。這可用於在需要執行一些額外清理工作(在物件呼叫 .finalize() 方法之前)之前保持物件處於活動狀態。

與其他兩種引用型別不同,“虛”引用必須與失效佇列關聯。

附錄 – Dianne Hackborn的vref提案(1995)

[此處已縮排並重新排列段落,但內容沒有改變。——弗雷德]

提案:虛擬引用

為了部分解決關於引用計數與垃圾回收的反覆討論,我提議對 Python 進行擴充套件,這應該有助於建立“結構良好”的迴圈圖。特別是,它應該允許至少建立帶有父後向指標的樹和雙向連結串列,而無需擔心迴圈。

我提議的基本機制是“虛擬引用”,或簡稱“vref”。vref 本質上是對一個物件的控制代碼,它不增加物件的引用計數。這意味著持有對一個物件的 vref 不會阻止該物件被銷燬。例如,這將允許 Python 程式設計師建立上述樹結構,當它不再使用時會自動銷燬——透過將所有父後向引用轉換為 vref,它們不再建立阻止樹銷燬的引用迴圈。

為了實現這種機制,Python 核心必須確保永遠不會留下任何實際的指標引用不再存在的物件。我提出的實現涉及對當前 Python 系統的兩個基本補充

  1. 一種新的“vref”型別,Python 程式設計師透過它建立和操作虛擬引用。在內部,它基本上是一個 C 級 Python 物件,帶有一個指向它所引用的 Python 物件的指標。然而,與所有其他 Python 程式碼不同,它不改變此物件的引用計數。此外,它包括兩個指標來實現雙向連結串列,下面將用到這個連結串列。
  2. 在基本 Python 物件中新增一個新欄位(PyObject_Head 在 object.h 中),該欄位要麼是 NULL,要麼指向所有引用它的 vref 物件列表的頭部。當一個 vref 物件將自身附加到另一個物件時,它會將自身新增到此連結串列中。然後,如果一個帶有任何 vref 的物件被解除分配,它可以遍歷此列表並確保其上所有 vref 都指向某個安全值,例如 Nothing。

這種實現有望對當前 Python 核心產生最小的影響——當不存在 vref 時,它應該只為所有物件新增一個指標,並在每次物件解除分配時檢查一個 NULL 指標。

回到 Python 語言層面,我考慮了 vref 物件的兩種可能的語義——

指標語義

在此模型中,vref 本質上表現得像一個 Python 級別的指標;Python 程式必須顯式地解引用 vref 才能操作它實際引用的物件。

使用此模型的一個 vref 模組示例可以包含函式“new”;當用作“MyVref = vref.new(MyObject)”時,它返回一個新的 vref 物件,使得 MyVref.object == MyObjectMyVref.object 然後會在 MyObject 被解除分配時變為 Nothing。

舉一個具體的例子,我們可以引入一些新的C風格語法

  • & – 一元運算子,建立物件的vref,與 vref.new() 相同。
  • * – 一元運算子,解引用vref,與 VrefObject.object 相同。

然後我們可以定義

1.     type(&MyObject) == vref.VrefType
2.        *(&MyObject) == MyObject
3. (*(&MyObject)).attr == MyObject.attr
4.          &&MyObject == Nothing
5.           *MyObject -> exception

規則 #4 很微妙,但它的產生是因為我們建立了一個指向(沒有實際引用的 vref)的 vref。因此,當內部 vref 不可避免地消失時,外部 vref 會被清除為 Nothing。

代理語義

在這個模型中,Python 程式設計師操作 vref 物件,就像她在操作它所引用的物件一樣。這是透過實現 vref 來實現的,使得對其進行的所有操作都被重定向到其引用的物件。在這個模型中,解引用運算子 (*) 不再有意義;相反,我們只有引用運算子 (&),並定義

1.  type(&MyObject) == type(MyObject)
2.        &MyObject == MyObject
3. (&MyObject).attr == MyObject.attr
4.       &&MyObject == MyObject

同樣,規則 #4 很重要——在這裡,外部 vref 實際上是對原始物件的引用,而不是內部 vref。這是因為對 vref 應用的所有操作實際上都應用於其物件,因此建立 vref 的 vref 實際上會導致建立後者物件的 vref。

第一種,指標語義,其優點是非常容易實現;vref 型別極其簡單,最少只需要一個屬性 object 和一個建立引用的函式。

然而,我真的很喜歡代理語義。它不僅減輕了 Python 程式設計師的負擔,而且還允許你做一些很棒的事情,比如在任何使用實際物件的地方使用 vref。不幸的是,在當前的 Python 實現中實現它可能會非常痛苦,甚至幾乎不可能。不過,如果這看起來很有趣,我確實有一些想法,其中一種可能性是引入新的型別檢查函式來處理 vref。這有望使不期望 vref 的舊 C 模組簡單地返回型別錯誤,直到它們可以修復。

最後,這個系統還可以提供一些其他功能。其中一個對我來說特別有趣的功能是允許 Python 程式設計師向 vref 新增“解構函式”——這個 Python 函式將在被引用物件被解除分配之前立即呼叫,允許 Python 程式不可見地將自身附加到另一個物件並監視它消失。這看起來很棒,儘管我還沒有想出任何實際用途…… :)

——Dianne


來源:https://github.com/python/peps/blob/main/peps/pep-0205.rst

最後修改:2025-02-01 08:59:27 GMT