在Python中安全使用解構函式
作者: Eli Bendersky
原文連結:https://eli.thegreenplace.net/2009/06/12/safely-using-destructors-in-python
本文適用於Python 2.5與2.6——如果你看到Python 3有任何不同,請讓我知道。
在C++中,解構函式是一個非常重要的概念,它們是RAII(resource acquisition is initialization)的一個基本成分——在丟擲異常的程式中,基本上是編寫涉及資源分配與釋放程式碼僅有的安全方式。
在Python中,解構函式的需求少得多,因為Python有進行記憶體管理的垃圾收集器。不過,雖然記憶體是最常見的分配資源,它不是唯一的。還有要關閉的套接字與資料庫連線,要重新整理的檔案、緩衝與快取,以及在一個物件用完時需要釋放的另外幾種資源。
因此Python有解構函式的概念——__del__方法。出於某個原因,Python社群裡的許多人認為__del__是邪惡的,不應該使用。不過,簡單grep標準庫顯示,在我們使用且喜歡的類中使用了數以十計的__del__,那麼要點在哪裡?在本文中,我將嘗試澄清它(首先是為我自己),何時應該使用__del__,以及如何使用。
簡單的例子程式碼
首先一個基本例子:
class FooType(object):
def __init__(self, id):
self
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.