如果我們要讓一個物件持有一些資源,然後希望這個物件被回收的時候釋放它持有的資源,我們該怎麼撰寫這個類別呢?舉例來說,TemporaryDirectory 類別會在建構實例的時候,使用 tempfile.mkdtemp()
產生一個暫時資料夾。當 TemporaryDirectory
的實例死亡後,會呼叫 shutil.rmtree()
刪除對應的暫時資料夾。我們該怎麼實作 TemporaryDirectory
呢?
簡單的想法
一個簡單的想法是定義類別的建構函式與解構函式。我們可以在 __init__()
方法(建構函式)中呼叫 tempfile.mkdtemp()
;另外在 __del__()
方法(解構函式)呼叫 shutil.rmtree()
:
#!/usr/bin/env python3
import shutil
import tempfile
class TemporaryDirectory(object):
def __init__(self):
self.name = tempfile.mkdtemp()
print('__init__:', self.name)
def __del__(self):
print('__del__:', self.name)
shutil.rmtree(self.name)
這段程式碼雖然可行,但從編寫風格的角度來評論,並不是一段好的程式碼。首先,__del__()
是一個有缺陷的語言構件,我們應該盡可能避免在類別中定義 __del__()
方法。其次,我們不能保證 __del__()
一定會被呼叫,若一個繼承 TemporaryDirectory
的子類別覆寫了 __del__()
方法,卻忘記呼叫 super().__del__()
,則該子類別的實例會產生資源洩漏。
我們要如何改進這段程式碼呢?
使用 weakref.finalize 類別
Python 3.4 以後,weakref
模組多了一個 finalize
類別。一言以蔽之,finalize
物件會記錄一個「對應物件」、一個 callback 函式與若干個稍後會被傳給 callback 函式的參數。當「對應物件」被回收並被解構之後,finalize
物件會呼叫 callback 函式,讓我們得以釋放資源。
我們可以將先前的 TemporaryDirectory
改寫為:
#!/usr/bin/env python3
import shutil
import tempfile
import weakref
class TemporaryDirectory(object):
def __init__(self):
self.name = tempfile.mkdtemp()
self.__finalizer = weakref.finalize(self, self._cleanup, self.name)
print('__init__:', self.name)
@staticmethod
def _cleanup(name):
print('_cleanup:', name)
shutil.rmtree(name)
實際執行這段程式碼:
def main():
t1 = TemporaryDirectory()
if __name__ == '__main__':
main()
我們可以得到類似的結果:
__init__: /tmp/tmpy_13a7z8
_cleanup: /tmp/tmpy_13a7z8
和先前「簡單的想法」相比,finalize
的實作保證一定會在 TemporaryDirectory
真的死亡後,才會呼叫 _cleanup()
;另外,_cleanup()
是一個靜態方法,它一定不會碰到已經死亡的 TemporaryDirectory
物件。
值得注意的是:我們不應把 TemporaryDirectory
的實例當作 callback 的參數。如果 finalize
的「對應物件」同時是 callback 的參數,則「對應物件」永遠不會被回收。
提前釋放資源
如果我們想要提前釋放資源,則可以使用 finalize
物件的 detach()
方法。finalize
物件本身會記錄「對應物件」的狀態。若根據 finalize
物件的記錄,其對應物件是存活的,則 detach()
方法會將先內部記錄改為死亡(但不會修改對應物件本身),再回傳「對應物件、callback 函式、callback 函式的參數、callback 函式的關鍵字參數」。若根據 finalize
物件的記錄,其對應的物件是死亡的,則 detach()
會回傳 None
。
作為範例,讓我們為 TemporaryDirectory
類別加上 cleanup()
方法,讓使用者可以透過呼叫 cleanup()
直接刪除暫時資料夾:
#!/usr/bin/env python3
import shutil
import tempfile
import weakref
class TemporaryDirectory(object):
def __init__(self):
self.name = tempfile.mkdtemp()
self.__finalizer = weakref.finalize(self, self._cleanup, self.name)
print('__init__:', self.name)
@staticmethod
def _cleanup(name):
print('_cleanup:', name)
shutil.rmtree(name)
def cleanup(self):
print('cleanup:', self.name)
if self.__finalizer.detach():
shutil.rmtree(self.name)
實際執行這段程式碼:
def main():
t1 = TemporaryDirectory()
t1.cleanup()
t2 = TemporaryDirectory()
if __name__ == '__main__':
main()
我們會在標準輸出看到以下結果:
__init__: /tmp/tmp_kp3odef
cleanup: /tmp/tmp_kp3odef
__init__: /tmp/tmpp0nebtt0
_cleanup: /tmp/tmpp0nebtt0
如同我們預期的,若使用者有呼叫 cleanup()
方法,finalize
物件就不會呼叫 _cleanup()
函式。如果使用者沒有呼叫 cleanup()
方法,則 finalize
物件會在對應物件(t2
指向的物件)死亡後,執行 _cleanup()
函式。
結語
在這篇文章我們提到 weakref.finalize
類別,並簡單地介紹它的使用方法。若我們需要在物件死亡後釋放物件所持有的資源,就可以善用 weakref.finalize
類別。
然而因為篇幅有限,這篇文章遺漏了兩個重要事項:
- 除了
__del__
方法可能會被子類別不正確地覆寫,本文並沒有解釋為什麼筆者認為__del__()
方法是設計不良的語言構件。 - 在舊版的 Python 中,
weakref
模組並沒有finalize
類別,我們是否有替代品?
這些內容先保留到後續的文章。敬請期待。