Numpyでとあるデータを永続化したときにNumpyのnp.savez()
がとんでもないことになっていた。
- Python 3.8.2
- Numpy 1.18.2+mkl
結果
結果は次のようになる。dictをアンパックせずにnp.savez()
のargsに指定してはいけない。
records = {
'dimens': (2, 5, 2, 5, 3,),
'matrices': np.random.randn(10, 10, 3)
}
np.savez('filename', records) # ココ!
with np.load('filename.npz', allow_pickle=True) as npz:
loaded_records = npz['arr_0'].flat[0]
dimens = loaded_records['dimens']
matrices = loaded_records['matrices']
この場合の正解は以下のようになる。
records = {
'dimens': (2, 5, 2, 5, 3,),
'matrices': np.random.randn(10, 10, 3)
}
np.savez('filename', **records) # ココ!
with np.load('filename.npz', allow_pickle=True) as loaded_records:
dimens = loaded_records['dimens']
matrices = loaded_records['matrices']
Pythonではアステリスク2つでdictをアンパックしてキーワード引数に出来る。こうすると非常に直観的にnp.load()
で構造化したデータを永続化して読み出せる。構造化したデータの読出しにはallow_pickle=True
が必要だがこれはエラーメッセージが教えてくれるのでハマりどころではない。
問題
既にミスしたケースで保存してしまっていたデータがあったのでなんとしてもこのデータを取り出す必要があった。問題はハマったケースの
loaded_records = npz['arr_0'].flat[0]
で何が起こっているのか。このケース、まずargs=records
をキーワードを省略してnp.savez()
に指定したことになる。この場合は順番にarr_0
,arr_1
,arr_2
というキーが割り当てられていくのでそこからndarrayを取り出せる。あくまでargsにはndarrayが指定されることが想定されているのだ。
ところが今指定したのはndarrayではなくdictを指定した。従ってこのnpz['arr_0']
はdtype=objectでdictの要素を1つもつ配列になっている。実際このことはこのオブジェクトをprintすれば表示される。問題はここからで、[0]
でそのdictを取り出すことが出来ると思うだろう。ところが
In [2]: npz['arr_0'][0]
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-53-770d95b9ee81> in <module>
----> 1 records[0]
IndexError: too many indices for array
どういうこと?となった。調べるとこういうことだった。
In [3]: npz['arr_0'].ndim, npz['arr_0'].shape, npz['arr_0'].size
Out[3]: (0, (), 1)
ndimとshapeは初期化されてないのにsizeは1でしっかりデータが入っている。なんやかんやした結果.flat
を経由してそのあるはずのデータにアクセスできることが分かったとさ。
感想
アンパックしなかったこっちのミスなんだけどもこの現象はひどすぎるよぉ。
https://docs.scipy.org/doc/numpy/reference/generated/numpy.savez.html
https://docs.scipy.org/doc/numpy/reference/generated/numpy.generic.html