注意: 雖然 JavaScript 對於本網站不是必需的,但您與內容的互動將受到限制。請開啟 JavaScript 以獲得完整的體驗。

模組清理的改進建議

模組清理的改進建議

我正在嘗試一種在執行結束後更好地清理資源的方法。在不實現真正GC(垃圾回收)的情況下,我永遠無法做到百分之百正確,但我可以根據實際觀察結果,實現一套可預測的規則,以解決大多數實際存在的問題。

這是我的提案。在本郵件的末尾,我列出了該提案可能存在的一些問題,並請求反饋。這可能將在Python 1.5.1中實現。

目錄

修訂版

根據我收到的一些評論和進一步的思考,我自上次在網上釋出此主題以來,對它做了一些修改。文字中用 [方括號中的斜體批註] 表示重要的更改。

演算法

當一個Python直譯器被刪除時,它的變數和模組會以部分指定的順序“小心地清除”。“小心地清除”操作的定義如下;它有效地以部分指定的順序刪除模組的變數。

  • M1. 在其他任何操作之前,以下變數被設定為None(不一定按此順序)
    • __builtin__._
    • sys.exc_{type,value,traceback}
    • sys.last_{type,value,traceback}
    • sys.path
    • sys.argv
    • sys.ps1, sys.ps2
    • sys.exitfunc
    [path, argv, ps1, ps2 和 exitfunc 是此列表中的新增項。]

  • M2. 三個標準I/O檔案(sys.stdin, sys.stdout 和 sys.stderr)恢復到它們的初始值(當直譯器啟動時,它們分別被儲存為 sys.__stdin__、sys.__stdout__ 和 sys.__stderr__)。如果任何初始值不可用,則相應的物件被設定為 None。[新增。]

  • M3. 在其他任何模組之前,小心地清除模組 __main__。[這以前是在下一步之後完成的。]

  • M4. 反覆遍歷所有模組,尋找引用計數為1的模組。每個引用計數為1的模組都被小心地清除。當不再找到引用計數為1的模組時,迴圈停止。模組 __builtin__ 和 sys 被排除在迴圈之外。

  • M5. 小心地清除所有剩餘模組,除了 __builtin__ 和 sys。

  • M6. 小心地清除 sys。

  • M7. 小心地清除 __builtin__。

要小心地清除一個模組,需要執行以下步驟:

  • C1. 按照名稱的字典雜湊值確定的順序,將所有恰好以一個下劃線開頭的名稱設定為 None。

  • C2. 按照名稱的字典雜湊值確定的順序,將除 __builtins__ 之外的所有名稱設定為 None。[這以前是“所有不以兩個或更多下劃線開頭的名稱”。]

  • [已刪除的步驟:按照名稱的字典雜湊值確定的順序,從模組的字典中刪除所有剩餘的名稱(透過呼叫 __dict__.clear() 完成)。]

  • C3. 模組本身在模組字典 (sys.modules) 中被 None 替換。

[新增。] 步驟 C1-C2 也將在模組解除分配時使用。雖然模組通常不涉及迴圈(除非存在相互遞迴的匯入),但模組的字典通常涉及迴圈,因為模組中定義的每個函式和方法都引用其 __dict__,並且這些函式和方法通常可從該 __dict__ 訪問。因此,當模組被刪除時,我會明確地“小心地”清除其 __dict__。(這始終在進行,只是以前沒有“小心地”進行。)

動機

執行M1是因為這些變數是使用者值常見的隱藏位置,並且它們在提議的順序中出現得太晚了。(事實上,幾乎所有報告的解構函式未按預期呼叫的問題都與這些變數有關。)

M3的存在是因為 __main__ 在概念上是程式的“根”——如果它沒有被其他模組匯入,它無論如何都會在M4步中首先被刪除,但如果它**被**其他地方匯入,刪除 __main__ 是一種打破僵局的合理方式。

M4 是一個顯式的垃圾回收迴圈——它刪除所有沒有被其他模組引用,只被模組表(sys.modules)本身引用的模組。然而,當存在相互匯入時,它可能不會刪除所有模組;剩餘的步驟將處理這些情況。

M5 需要處理相互遞迴的匯入,這些匯入會建立迴圈,導致 M4 無法刪除所有內容。

對 __builtin__ 和 sys 的特殊處理是因為它們被直譯器透過許多操作隱式引用;__builtin__ 當然包含所有內建函式和異常;sys 包含標準 I/O 檔案,這些檔案被各種 I/O 操作隱式引用。所以它們在 M2 和 M4 中被排除。__builtin__ 最後被刪除,因為它包含最基本和最核心的值。

模組字典清理的特殊處理是必要的,因為當模組定義 Python 函式或類時,會存在一個基本的迴圈引用。函式物件包含對其“全域性”物件的引用,該物件是定義它的模組的 __dict__。由於 __dict__ 通常有一個對函式的引用,因此存在一個需要打破的迴圈,否則 __dict__ 將永遠不會被垃圾回收。

請注意,基於引用計數的解決方案在一個模組內不起作用,因為函式之間的引用是按名稱而不是按值進行的——兩個相互遞迴的函式仍然可以都具有一個引用計數,因為它們相互執行名稱查詢。

C1 旨在為模組提供一種方式來定義在模組中其他任何東西之前被刪除的全域性變數。由於匯入的模組或函式名稱通常不以下劃線開頭,這意味著可以保證,當這些物件被刪除時,它們可能需要的任何匯入模組或函式仍然存在——當然,前提是對它們的唯一引用是在模組中。(此步驟已在釋出的 1.5 版本中實現。)

C2 刪除剩餘物件,但保留“內部全域性變數”__builtins__ 不動——這避免了 1.5 版本中存在的問題,例如在解構函式中使用“None”會引發 NameError!

C3 從模組表中刪除對模組的引用,從而使稍後匯入同一模組的操作失敗。(使用者程式碼可以刪除此條目並仍然啟動一個全新的匯入——但如果他們如此聰明,那麼他們活該。)

問題和疑問

P1. 當模組 M 的所有使用形式都是 ``from M import ...'' 時,模組 M 的引用計數將為 1。因此它將在 M1 步驟中被刪除。這使得模組中除最微不足道(可能仍被其他模組引用)的函式之外的所有函式都變得無用,因為它們可能需要的匯入模組和函式都已從它們的全域性變數中刪除。當然,一個簡單的補救措施是不使用 ``from M import ...'',但這聽起來可能會成為一個常見問題……問題是我不知道更好的方法——由於函式及其模組的 __dict__ 之間存在迴圈引用,我無法在 M1 步驟中使用 __dict__ 的引用計數。我認為這是可以接受的——這種行為在 1.4 及更早版本中也存在。(曾有人建議在匯入模組中新增一個名為例如 ".module" 的引用,以表明依賴關係並防止此問題;雖然這可能很好地完成任務,但我猶豫不決地實現它,因為它可能會混淆內省工具。)