[譯]在Python中安全使用解構函式
作者:Eli Bendersky
本文適用於 Python 2.5 與 2.6—— 如果你看到 Python 3 有任何不同,請讓我知道。
在 C++ 中,解構函式是一個非常重要的概念,它們是 ofollow,noindex" target="_blank">RAII ( resource acquisition is initialization )的一個基本成分——在丟擲異常的程式中,基本上是編寫涉及資源分配與釋放程式碼僅有的安全方式。
在 Python 中,解構函式的需求少得多,因為 Python 有進行記憶體管理的垃圾收集器。不過,雖然記憶體是最常見的分配資源,它不是唯一的。還有要關閉的套接字與資料庫連線,要重新整理的檔案、緩衝與快取,以及在一個物件用完時需要釋放的另外幾種資源。
因此 Python 有解構函式的概念—— __del__ 方法。出於某個原因, Python 社群裡的許多人認為 __del__ 是邪惡的,不應該使用。不過,簡單 grep 標準庫顯示,在我們使用且喜歡的類中使用了數以十計的 __del__ ,那麼要點在哪裡?在本文中,我將嘗試澄清它(首先是為我自己),何時應該使用 __del__ ,以及如何使用。
簡單的例子程式碼
首先一個基本例子:
class FooType ( object ):
def __init__ ( self , id ):
self .id = id
print self .id, 'born'
def __del__ ( self ):
print self .id, 'died'
ft = FooType( 1 )
這打印出:
1 born
1 died
現在,回憶由於一個引用計數垃圾收集器的使用, Python 在一個物件退出作用域時,不會清理它。在該物件的最後一個引用退出作用域時,才將清理它。下面是一個展示:
class FooType ( object ):
def __init__ ( self , id ):
self .id = id
print self .id, 'born'
def __del__ ( self ):
print self .id, 'died'
def make_foo ():
print 'Making...'
ft = FooType( 1 )
print 'Returning...'
return ft
print 'Calling...'
ft = make_foo()
print 'End...'
這打印出:
Calling...
Making...
1 born
Returning...
End...
1 died
在程式終止時呼叫了這個解構函式,不是在 ft 退出 make_foo 裡的作用域時。
解構函式的替代品
在我繼續之前,一個合適的揭露:對資源的管理, Python 提供了比解構函式更好的方法——上下文( context )。我不會把這變成上下文的一個教程,但你應該熟悉 with 語句,以及可以在內部使用的物件。例如,處理檔案寫入的最好方法是:
with open ( 'out.txt' , 'w' ) as of:
of.write( '222' )
這確保在退出 with 內部的程式碼塊時,該檔案被正確關閉,即使丟擲異常。注意這展示了一個標準的上下文管理器。另一個是 threading.lock ,它返回一個非常適合在一個 with 語句中使用的上下文管理器。更多細節,閱讀 PEP 343 。
雖然推薦, with 不總是適用的。例如,假設你有一個封裝了某種資料庫的物件,在該物件生命期結束時,必須提交併關閉該資料庫。現在,假定該物件應該是某種大且複雜的類(比如一個 GUI 會話,或者一個 MVC 模型類)的一個成員變數。父親在別的方法中不時地與該 DB 物件互動,因此使用 with 是不現實的。所需要的是一個起作用的解構函式。
解構函式何處走偏
為了解決我在上一段展示的用例,你可以採用 __del__ 解構函式。不過,知道這不總是工作良好是重要的。引用計數垃圾收集器的死對頭是迴圈引用。下面是一個例子:
class FooType ( object ):
def __init__ ( self , id , parent):
self .id = id
self .parent = parent
print 'Foo' , self .id, 'born'
def __del__ ( self ):
print 'Foo' , self .id, 'died'
class BarType ( object ):
def __init__ ( self , id ):
self .id = id
self .foo = FooType( id , self )
print 'Bar' , self .id, 'born'
def __del__ ( self ):
print 'Bar' , self .id, 'died'
b = BarType( 12 )
輸出:
Foo 12 born
Bar 12 born
噢……發生了什麼?解構函式在哪裡?下面是 Python 文件在這件事上的陳述:
在啟用了可選的迴圈檢測器(預設開啟)時,檢測垃圾的迴圈引用,但僅在不涉及 Python 層面的 __del__() 方法時,才能被清理。
Python 不知道銷燬彼此持有迴圈引用的物件的安全次序,因此作為一個設計決策,它只是不對這樣的方法呼叫解構函式!
那麼,現在怎麼辦?
因為其缺陷,我們不應該使用解構函式嗎?我非常吃驚地看到許多 Python 支持者認為這樣,並建議使用顯式的 close 方法。但我不同意——顯式的 close 方法不那麼安全,因為它們容易忘記呼叫。另外,在發生異常時(在 Python 裡,它們隨時出現),管理顯式關閉變得非常困難且煩人。
我確實認為解構函式可以且應該在 Python 裡被安全地使用。帶著幾分小心,這絕對可能。
首先以及最重要的,注意到合理的迴圈引用是罕見的。我故意說合理的( justified )——出現迴圈引用的大量使用是壞的設計以及有漏洞抽象的樣本。
作為一個經驗規則,資源儘可能由最底層的物件持有。不要在你的 GUI 會話裡直接持有一個 DB 資源。使用一個物件封裝這個 DB 連線,並在解構函式裡安全地關閉它。 DB 物件沒有理由持有你程式碼裡其他物件的引用。如果這樣——它違反了幾個好的設計實踐。
有時,在複雜程式碼中,依賴性注入( dependency injection )有助於防止迴圈引用,不過即使在你發現需要一個真迴圈引用的罕見情形裡,也存在解決方案。 Python 為此提供了 weakref 模組。文件很快揭示,這正是我們這裡所需要的:
一個物件的弱引用不足以保持物件存活:當一個被引用物件僅有的引用是弱引用時,垃圾收集可以自由地銷燬這個被引用物件,併為其他物件重用其記憶體。弱引用的主要使用是實現快取或持有大物件的對映,其中期望大物件不僅僅因為出現在快取或對映中,而被保持存活。
下面是用 weakref 重寫的前面的例子:
import weakref
class FooType ( object ):
def __init__ ( self , id , parent):
self .id = id
self .parent = weakref.ref(parent)
print 'Foo' , self .id, 'born'
def __del__ ( self ):
print 'Foo' , self .id, 'died'
class BarType ( object ):
def __init__ ( self , id ):
self .id = id
self .foo = FooType( id , self )
print 'Bar' , self .id, 'born'
def __del__ ( self ):
print 'Bar' , self .id, 'died'
b = BarType( 12 )
現在我們得到希望的結果:
Foo 12 born
Bar 12 born
Bar 12 died
Foo 12 died
這個例子裡的小改動是,在 FooType 建構函式裡,我使用 weakref.ref 對 parent 引用賦值。這是一個弱引用,因此它不會真正建立一個環。因此 GC 看不到環,它銷燬了這兩個物件。
結論
Python 有經由 __del__ 方法的完美、可用的物件解構函式。對絕大多數用例,它工作良好,但堵塞在迴圈引用處。不過,迴圈引用通常是壞設計的一個跡象,它們很少是合理的。對極少數使用了合理的迴圈引用的用例裡,使用弱引用很容易打破迴圈, Python 在 weakref 模組裡提供弱引用。
參考文獻
在準備本文時,某些有用的連結:
- Python destructor and garbage collection notes
- RAII
- The Python documentation
- This and also this Stack Overflow discussions.