はじめに
初めてQiitaに投稿します。よろしくお願いします。
numpyのndarrayを格納したファイルを読み込むnumpy load()
は、単一のアレイを格納した.npy
ファイルと複数のアレイを格納した.npz
ファイルの両方に対応しています。このうち後者のケースではdict-likeなオブジェクトが返されるということは何となくわかっていたのですが、それをdictのつもりで扱ったところ思い通りの結果が得られないという事態が発生しました。今回はそれをきっかけに調べてわかったことを書いてみます。
環境
以下の環境で確認しました。
- OS: Ubuntu 22.04.5 LTS (Windows 11のWSL2)
- Python: 3.12.10
- Numpy: 2.2.5
以下、プロンプトが >>> であるものはPythonの対話モード、$であるものはBashの画面です。
準備
簡単なデータを作ってnp.savez()
で保存します。保存の際には名前を指定しておきます。
>>> import numpy as np
>>> a = np.array([1, 2, 3])
>>> b = np.array([4, 5, 6])
>>> np.savez("data", a=a, b=b)
読み込みます。
>>> data_npz = np.load("data.npz")
>>> type(data_npz)
<class 'numpy.lib.npyio.NpzFile'>
読み込まれたのはNpzFileオブジェクトです。
dict-likeな性質
保存時に指定した名前で値を取り出せます。
>>> data_npz["a"]
array([1, 2, 3])
>>> data_npz["b"]
array([4, 5, 6])
.keys()
、.values()
、.items()
も使えます。
>>> for key in data_npz.keys():
... print(key)
...
a
b
>>> for value in data_npz.values():
... print(value)
...
[1 2 3]
[4 5 6]
>>> for key, value in data_npz.items():
... print(key, value)
...
a [1 2 3]
b [4 5 6]
dictじゃない性質
値を書き換えることができません。itemそのものを入れ替えようとするとエラーになります。
>>> data_npz["a"] = np.array([0, 2, 3])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'NpzFile' object does not support item assignment
itemの中身を書き換えようとするとエラーにはなりません。でも値は書き換わりません。
>>> data_npz["a"]
array([1, 2, 3])
>>> data_npz["a"][0] = 0
>>> data_npz["a"]
array([1, 2, 3])
冒頭書いた「思い通りの結果が得られなかった」というのはこれが原因でした。.npz
ファイルから読み込んだデータを処理する既存のプログラムがあり、読み込むファイルの一部のデータに誤りがあるのでパッチ的に書き換えたい、という状況が発生したため、後続の処理を変えなくて済むように読み込んだオブジェクトを書き換える処理を追加してみたのですが、修正が反映されなかったのです。しばらく悩んでしまいました。
ちなみに各itemを他の変数に代入すれば普通に書き換えできます。
>>> a2 = data_npz["a"]
>>> a2
array([1, 2, 3])
>>> a2[0] = 0
>>> a2
array([0, 2, 3])
dictとして扱いたい場合はdict()
で変換するという手もあります。上述の件はこのやり方で対処しました。
>>> data2 = dict(data_npz)
>>> data2
{'a': array([1, 2, 3]), 'b': array([4, 5, 6])}
>>> data2["a"][0] = 0
>>> data2
{'a': array([0, 2, 3]), 'b': array([4, 5, 6])}
ファイルオープンの問題
私がハマった点は上述の通りですが、実際にはこちらの方が大事かもしれません。numpy.load()
で.npz
ファイルを読み込んだ場合、ファイルはオープンされたままになります。上記の例を実行した後に別のシェルを開いてlsofで確認したのがこちら。
$ lsof data.npz
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python 21595 yoshimura 4r REG 8,32 538 388163 data.npz
オープンされています。だから、例えばこんな風に5個ファイルを作って
$ for i in 1 2 3 4 5; do cp data.npz data_${i}.npz; done
それぞれの内容を読み込んで配列に入れて
>>> data_list = []
>>> for i in range(1, 6):
... d = np.load(f"data_{i}.npz")
... data_list.append(d)
...
lsofしてみると全部開いたままです。
$ lsof data_*.npz
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python 22147 yoshimura 3r REG 8,32 538 19978 data_1.npz
python 22147 yoshimura 4r REG 8,32 538 21746 data_2.npz
python 22147 yoshimura 5r REG 8,32 538 21788 data_3.npz
python 22147 yoshimura 6r REG 8,32 538 21851 data_4.npz
python 22147 yoshimura 7r REG 8,32 538 24330 data_5.npz
うっかり数千個のファイルを読もうとして、ファイルオープンの上限を超えてしまってエラーになる、ということも十分あり得ます。
numpy API Referenceのnumpy.loadの項を見ると以下の記述がありました。
- Returns:
- result
- array, tuple, dict, etc. Data stored in the file. For .npz files, the returned instance of NpzFile class must be closed to avoid leaking file descriptors.
numpy.lib.npyio.NpzFileも確認してみましょう。
A dictionary-like object with lazy-loading of files in the zipped archive provided on construction.
なるほど。遅延ロードということは、NpzFileオブジェクトはファイルの中身へのアクセスを提供するものだと認識すべきものなのですね。それなら書き換えられないことも、生きている間はファイルオープンされたままになることも、当然であると理解できます。
ファイルクローズするには読み込んだオブジェクトを.close()
する必要がありますが、with
ブロックも使えるのでこちらの方が簡単で安全です (API ReferenceのNotes参照)。
>>> with np.load("data.npz") as d:
... a = d["a"]
... b = d["b"]
...
>>> a
array([1, 2, 3])
>>> b
array([4, 5, 6])
ちなみに.npy
は?
順序が後先になりますが、単純にndarrayをsaveした.npy
の場合も確認しておきます。
>>> np.save("data_a", a)
>>> a_npy = np.load("data_a.npy")
>>> type(a_npy)
<class 'numpy.ndarray'>
>>> a_npy
array([1, 2, 3])
>>> a_npy[0] = 0
>>> a_npy
array([0, 2, 3])
得られるのは単純なndarrayです。書き換えもできますし、ファイルは自動的に閉じられます。逆にwith
を使うとエラーになります。ndarrayですから当然と言えば当然ですが。
>>> with np.load("data_a.npy") as d:
... a = d
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'numpy.ndarray' object does not support the context manager protocol
まとめ
.npz
ファイルをnumpy.load()
で読み込んで得られるNpzFileオブジェクトには、個々のitemに対するdict-likeなデータアクセス手段が備わっていますが、データをdictにまとめたものとは似て非なるもので、ファイルの中身へのアクセスを提供するものだと認識すべきです。特に、データを書き換えられないことや、生きている間はファイルオープンされたままになることには注意が必要です。with
ブロックを使用することで簡単かつ安全に取り扱うことができます。