模組清理的改進建議
模組清理的改進建議
我正在嘗試一種更好的方法來在執行執行結束時進行清理。在不實現真正的 GC 的情況下,我永遠無法 100% 正確地完成它,但我可以基於實際觀察實現一組可預測的規則,這將解決實際觀察到的大部分問題。
這是我的建議。在本訊息末尾,我列出了一些該建議的潛在問題並徵求反饋。這可能會在 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
- 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 函式或類時,總是存在基本的迴圈引用。函式物件包含對函式“globals”物件的引用,該物件是定義它的模組的 __dict__。由於 __dict__ 通常有一個對函式的引用,因此存在一個需要打破的迴圈,否則 __dict__ 將永遠不會被垃圾回收。
請注意,基於引用計數的解決方案在一個模組內不起作用,因為函式之間的引用是按名稱而不是按值進行的——兩個相互遞迴的函式仍然可以都具有引用計數 1,因為它們會互相進行名稱查詢。
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” 的引用,以指示依賴關係並防止此問題;雖然這可能會很好地完成工作,但我不太願意實現它,因為它可能會使自省工具感到困惑。)