Python でリソース管理に __del__ を使うのは危険だ。一見 C++ のデストラクタのように見えるが、まったく異なる性質を持つ。
__del__ とは
__del__ はオブジェクトがガベージコレクタによって回収されるときに呼ばれるメソッドだ。ファイナライザとも呼ばれる。
class Resource: def __init__(self, name): self.name = name print(f"{name}: 作成") def __del__(self): print(f"{self.name}: 破棄") r = Resource("test") del r # "test: 破棄" が出力される...かもしれない
呼ばれるタイミングが保証されない
__del__ の最大の問題は、いつ呼ばれるかわからないことだ。
スコープを抜けた瞬間に確実に呼ばれる。タイミングは完全に予測可能
GC がオブジェクトを回収するときに呼ばれる。タイミングは処理系依存で予測不能
CPython では参照カウントがゼロになった瞬間に __del__ が呼ばれることが多いが、これは言語仕様ではなく実装の詳細だ。
def example(): r = Resource("test") # 関数を抜けると r の参照カウントがゼロになる # CPython では「たぶん」ここで __del__ が呼ばれる # PyPy や Jython では呼ばれないかもしれない
循環参照があると呼ばれない
循環参照がある場合、参照カウントだけでは回収できない。GC の循環参照検出が必要になるが、__del__ を持つオブジェクトは Python 3.3 以前では回収されなかった。
class Node: def __init__(self): self.next = None def __del__(self): print("Node 破棄") # 循環参照を作る a = Node() b = Node() a.next = b b.next = a del a del b # Python 3.3 以前: __del__ は呼ばれない # Python 3.4 以降: いつか呼ばれるが、タイミングは不明
Python 3.4 で PEP 442 により改善されたが、タイミングが不定という問題は残っている。
インタプリタ終了時の問題
プログラム終了時、__del__ が呼ばれる順序は保証されない。依存しているモジュールやオブジェクトがすでに破棄されている可能性がある。
import os class FileWrapper: def __init__(self, path): self.file = open(path, "w") def __del__(self): self.file.close() os.remove(self.file.name) # os モジュールがすでに None かもしれない
グローバル変数が None に置き換えられていて AttributeError が発生する。例外は黙って無視されるため、気づかないことも多い。
__del__ 内で発生した例外は stderr に出力されるだけで、プログラムの動作に影響しない。エラー処理が困難。
正しいリソース管理の方法
リソース管理にはコンテキストマネージャ(with 文)を使う。
class Resource: def __init__(self, name): self.name = name self.acquired = True print(f"{name}: 獲得") def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.release() return False def release(self): if self.acquired: print(f"{self.name}: 解放") self.acquired = False # with 文で確実にリソースが解放される with Resource("test") as r: print("リソース使用中") # ここで必ず release() が呼ばれる
__del__ を使ってよい場面
__del__ が完全に無意味なわけではない。
解放し忘れた場合の保険として
デバッグ用のログ出力として
C 拡張のメモリ解放として
ただし、これらも「あくまで補助」であり、主要なリソース管理手段にしてはいけない。
class Resource: def __enter__(self): return self def __exit__(self, *args): self.release() def release(self): # 主要な解放処理 pass def __del__(self): # 保険:release() が呼ばれていなければ警告 if not self.released: import warnings warnings.warn(f"{self} was not properly released")
結論
タイミング不定、循環参照で問題、終了時に不安定。リソースリークの原因になる
確実に解放される、例外があっても安全、Python の標準的なパターン
C++ の RAII に慣れた人が __del__ を使いたくなる気持ちはわかるが、Python では with 文がその役割を担う。__del__ はデストラクタではなくファイナライザであり、根本的に異なるものだと理解しておく必要がある。