__del__ に頼ったリソース管理の危険性
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__ はデストラクタではなくファイナライザであり、根本的に異なるものだと理解しておく必要がある。