__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__ の最大の問題は、いつ呼ばれるかわからないことだ。

C++ のデストラクタ

スコープを抜けた瞬間に確実に呼ばれる。タイミングは完全に予測可能

Python の __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")

結論

__del__ に頼る設計

タイミング不定、循環参照で問題、終了時に不安定。リソースリークの原因になる

with 文 + __enter__/__exit__

確実に解放される、例外があっても安全、Python の標準的なパターン

C++ の RAII に慣れた人が __del__ を使いたくなる気持ちはわかるが、Python では with 文がその役割を担う。__del__ はデストラクタではなくファイナライザであり、根本的に異なるものだと理解しておく必要がある。