Python でファイルを開いているときに例外が発生したら、そのファイルはどうなるのか。メモリリークは起きるのか。with 文を使えば本当に安全なのか。これらの疑問を整理する。
例外発生時にファイルはどうなるか
まず、with 文を使わない場合を見てみよう。
f = open("data.txt", "w")
f.write("hello")
raise Exception("何かのエラー") # ここで例外発生
f.close() # 実行されない
この場合、f.close() は実行されない。ファイルオブジェクトは閉じられないまま残る。
参照カウントがゼロになった時点でガベージコレクタがファイルを閉じる。ただし「いつ」閉じられるかは保証されない
参照カウント方式ではないため、GC が走るまでファイルは開いたまま。長時間閉じられない可能性がある
メモリリークは起きるか
厳密に言えば「メモリリーク」ではなく「リソースリーク」が起きる。
OS には同時に開けるファイル数の上限がある。閉じ忘れが続くと「Too many open files」エラーになる。
書き込みモードで開いたファイルは、close() されるまでバッファの内容がディスクに書き込まれない可能性がある。データ消失の原因になる。
ファイルオブジェクト自体のメモリは GC により回収される。ただし OS レベルのリソースは別問題。
with 文は完璧か
with 文を使えば、例外が発生しても __exit__ メソッドが呼ばれ、ファイルは確実に閉じられる。
with open("data.txt", "w") as f:
f.write("hello")
raise Exception("何かのエラー")
# ここで例外が発生しても、with を抜けるときに f.close() が呼ばれる
これは try-finally と等価だ。
f = open("data.txt", "w")
try:
f.write("hello")
raise Exception("何かのエラー")
finally:
f.close() # 例外が発生しても必ず実行される
では with 文は完璧か。ほぼ完璧だが、いくつかの落とし穴がある。
with 文の落とし穴
with open(...) の open() 呼び出し自体で例外が発生した場合、ファイルはそもそも開かれていないので問題ない。ただし複数ファイルを開く場合は注意が必要。
通常の例外と同様に __exit__ は呼ばれる。ただし SIGKILL でプロセスが強制終了された場合は何も実行されない。
__exit__ 自体が例外を発生させると、リソースが正しく解放されない可能性がある。ただしファイルオブジェクトの __exit__ は単純な close() なので、これが失敗することは稀。
複数ファイルを開く場合
複数ファイルを扱うときは、2 つ目の open() で例外が発生すると 1 つ目のファイルが閉じられない問題がある。
# 危険なパターン
with open("file1.txt") as f1:
with open("file2.txt") as f2: # ここで失敗すると f1 は閉じられる(OK)
pass
# Python 3.1+ ではまとめて書ける
with open("file1.txt") as f1, open("file2.txt") as f2:
pass
# この書き方なら、f2 の open() が失敗しても f1 は閉じられる
ただし、上記のネストした with 文でも実際には問題ない。外側の with 文の __exit__ が呼ばれるからだ。
del に頼るべきではない理由
「どうせ GC がファイルを閉じてくれる」と考えるのは危険だ。
GC のタイミングは予測不能
循環参照があると回収が遅れる
PyPy 等では特に遅延が顕著
プロセス終了時まで閉じられない可能性
CPython でも、関数を抜けた直後にファイルが閉じられる保証はない。明示的に閉じるか、with 文を使うのが正解だ。
結論
通常のプログラミングでは with 文で十分。例外が発生してもファイルは閉じられる。
SIGKILL による強制終了など、どうしようもないケースを除けば with 文は信頼できる。そもそも SIGKILL では何をやっても無駄。
with 文を使わない理由はない。ファイル操作では常に with 文を使い、リソース管理を Python に任せるのがベストプラクティス。