CUBE SUGAR CONTAINER

技術系のこと書きます。

Python: ファイルオブジェクトのクローズについて

Python でファイルシステム上にあるファイルを扱うには open() 関数などを使ってファイルオブジェクトを取得する。 しかし、使い終わったファイルオブジェクトはちゃんと close() メソッドを呼んで後片付けをしなければいけない。 そして、同時に開くファイルオブジェクトの数は少なくしなければいけない。 今回は一体なぜそんなことをしなければいけないのか、どうすればいいのかについて書いてみる。

使った環境は次の通り。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.11.1
BuildVersion:   15B42
$ python --version
Python 3.5.0

たくさん開いてみよう

クローズしなければいけない理由は、多くのファイルを開いたときにわかる。 Python の REPL を使って、そこらへんにあるファイルをひたすら開いてみよう。 ここでは '/dev/null' デバイスを使うことにする。 その際、取得したファイルオブジェクトはリストに格納することで GC (ガーベジコレクション) の対象にならないようにしよう。

やってみると OSError 例外になることがわかる。

$ python
>>> files = []
>>> while True:
...     file = open('/dev/null')
...     files.append(file)
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
OSError: [Errno 24] Too many open files: '/dev/null'

この状態になると、もうそれ以上ファイルを開くことはできない。

>>> open('/dev/null')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: [Errno 24] Too many open files: '/dev/null'

しかし、既存のファイルオブジェクトのひとつをクローズすると、新たにひとつだけファイルを開けるようになる。 つまり、同時に開けるファイルオブジェクトの数には上限があるということがわかった。

>>> files[0].close()
>>> open('/dev/null')
<_io.TextIOWrapper name='/dev/null' mode='r' encoding='UTF-8'>

開くことができたファイルオブジェクトの数をみると、何やら中途半端な数になっていることがわかる。 もっとも近い 2 の倍数でいうと 256 に 3 足りない。

>>> len(files)
253

この 3 足りない理由は明確で、標準入力、標準出力、標準エラー出力という 3 つのファイルオブジェクトをデフォルトで開いているから。

>>> import sys
>>> sys.stdin
<_io.TextIOWrapper name='<stdin>' mode='r' encoding='UTF-8'>
>>> sys.stdout
<_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>
>>> sys.stderr
<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>

数字の正体

では、一体この 256 という数字はどこからきているのか。 Python がファイルオブジェクトを開いている状態で、別のターミナルから lsof コマンドを実行してみよう。 このコマンドは各プロセスが開いているファイルの情報を見ることができる。

$ lsof -c python
COMMAND     PID    USER   FD     TYPE             DEVICE  SIZE/OFF    NODE NAME
...(省略)...
python3.5 81267 amedama  246r     CHR                3,2       0t0     304 /dev/null
python3.5 81267 amedama  247r     CHR                3,2       0t0     304 /dev/null
python3.5 81267 amedama  248r     CHR                3,2       0t0     304 /dev/null
python3.5 81267 amedama  249r     CHR                3,2       0t0     304 /dev/null
python3.5 81267 amedama  250r     CHR                3,2       0t0     304 /dev/null
python3.5 81267 amedama  251r     CHR                3,2       0t0     304 /dev/null
python3.5 81267 amedama  252r     CHR                3,2       0t0     304 /dev/null
python3.5 81267 amedama  253r     CHR                3,2       0t0     304 /dev/null
python3.5 81267 amedama  254r     CHR                3,2       0t0     304 /dev/null
python3.5 81267 amedama  255r     CHR                3,2       0t0     304 /dev/null

実行結果の中で FD という列の数字が 255 まで振られていることに気がつく。 この FD というのは File Descriptor (ファイル記述子) の略になっている。 つまり、ファイルオブジェクトひとつに対してこのファイル記述子というものがひとつ割り当てられている。

なぜ上限があるのかという疑問については、このファイル記述子というのものが OS の管理している有限なリソースだからだ。 つまり、各プロセスで好き勝手に使われると困る。

先ほど出てきた 256 という上限の値は Python からだと resource モジュールを使って確認できる。

$ python -c "import resource; print(resource.getrlimit(resource.RLIMIT_NOFILE)[0])"
256

コマンドラインで確認するときは ulimit コマンドを使う。

$ ulimit -n
256

試しに ulimit コマンドで上限を 64 に変更した場合の挙動を確認しておこう。 64 から 3 を引いた値の 61 個のファイルオブジェクトを開いておけば、次に開こうとしたときに例外になるはずだ。

$ ulimit -n 64
$ python
>>> files = [open('/dev/null') for _ in range(61)]
>>> len(files)
61

この試みは見事に上手くいく。

>>> open('/dev/null')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: [Errno 24] Too many open files: '/dev/null'

ちなみに ulimit で設定される上限はソフトリミットと呼ばれるもので、この他にカーネルパラメータで設定するハードリミットと呼ばれるものもある。 Mac OS X であれば kern.maxfiles が OS 全体のファイル記述子の上限で、kern.maxfilesperproc がプロセスあたりの上限 (ハードリミット) に対応しているようだ。 つまり、ひとつのプロセスが使えるファイル記述子の数の上限はソフトリミットとハードリミットという二段構えになっている。

$ sysctl -a | grep maxfile
kern.maxfiles: 12288
kern.maxfilesperproc: 10240

上限はプロセス単位でかかるので、別のターミナル (つまり別のプロセス) からであれば新たにファイルを開くこともできる。 もちろん、これは OS 全体のファイル記述子の上限に達していない場合に限る。

$ python
>>> open('/dev/null')
<_io.TextIOWrapper name='/dev/null' mode='r' encoding='UTF-8'>

で、結局どうすればいいのか

守らなければいけないことはふたつだけ。

  • ファイルオブジェクトを使い終わったら確実にクローズする
  • 一度にたくさんのファイルオブジェクトを開かない

ひとつ目は、失敗すると開いたままのファイル記述子がずっと放置されることになって、そのうち上限に達したりする。 これを FD リークと呼ぶ。 閉じ忘れを防ぐにはファイルオブジェクトを with 文と共に使おう。

>>> with open('/dev/null', mode='rb') as f:
...     f.read(1)
...
b''

ふたつ目はマルチスレッドで処理をしているときなどに、一度に開くファイルオブジェクトの数を常に意識しておかないと瞬間的に上限に達してしまう可能性がある。

GC に回収されたファイルオブジェクトはどうなる?

ここまで見てくると、クローズされていない状態で GC に回収されたファイルオブジェクトによって FD リークが起きないのか、という点が気になる。

まずは挙動から確認しておこう。 先ほどと同様に、まずは GC に回収されないようにリストに格納する形でファイルオブジェクトを上限まで開いておく。

>>> files = []
>>> while True:
...     file = open('/dev/null')
...     files.append(file)
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
OSError: [Errno 24] Too many open files: '/dev/null'

そして、ファイルオブジェクトの入ったリストの参照を開放する。 CPython であれば参照カウント方式にもとづいた GC を使っているので、この瞬間に回収される (はず) 。

>>> files = None

PyPy を使っている場合には、異なる方式の GC なので参照がなくなったからといってその瞬間に回収されることはない。 なので gc モジュールを使って明示的に実行するといいんじゃないだろうか。 ただし、仕様上は以下を実行しても回収される保証はない。

>>> import gc; gc.collect()  # PyPy を使っている場合
0

この状態で新たにファイルオブジェクトを開くと、ちゃんとファイルオブジェクトを取得できる。 つまり、どうやら GC に回収されたファイルオブジェクトは自動的にクローズされるようだ。

>>> open('/dev/null')
<_io.TextIOWrapper name='/dev/null' mode='r' encoding='UTF-8'>

この挙動が仕様にもとづいたものなのかを軽く調べてみたところ、次の公式ドキュメントの記述が見つかった。

11.6. tempfile — Generate temporary files and directories — Python 3.5.2 documentation

(including an implicit close when the object is garbage collected)

ということで GC に回収されたファイルオブジェクトが自動的にクローズされるのは仕様のようだ。