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 に回収されたファイルオブジェクトが自動的にクローズされるのは仕様のようだ。
スマートPythonプログラミング: Pythonのより良い書き方を学ぶ
- 作者: もみじあめ
- 発売日: 2016/03/12
- メディア: Kindle版
- この商品を含むブログを見る