numpy.arrayインプレース演算の落とし穴(Python)


概要

Python numpyでインプレース演算(+=など)をした際に思わぬ挙動をすることがあるので、
その確認をする。

内容

環境

macOS Catalina
Python 3.7.0
numpy 1.18.3

再現

>>> import numpy as np
>>> a = np.array([3])
>>> b, c = a, a
>>> a, b, c
(array([3]), array([3]), array([3]))
>>> c += 1
>>> a, b, c
(array([4]), array([4]), array([4]))

分析

a, b, cが参照しているのは、np.arrayオブジェクトに対してであるが、
+=1が実は変更したのは、その属性であるからである。
下記のように、+=1がオブジェクト自体を変更する場合は、予想通りの変化になる。

>>> a = 3
>>> b, c = a, a
>>> a, b, c
(3, 3, 3)
>>> c += 1
>>> a, b, c
(3, 3, 4)
>>>

解決策

c = c + 1とし、オブジェクトを新たに生成する。

>>> import numpy as np
>>> a = np.array([3])
>>> b, c = a, a
>>> c = c + 1
>>> a, b, c
(array([3]), array([3]), array([4]))

蛇足

id関数を使って確認することもできる。

>>> import numpy as np
>>> a = np.array([3])
>>> b, c = a, a
>>> id(a), id(b), id(c)
(4692042016, 4692042016, 4692042016)
>>> c += 1
>>> a, b, c
(array([4]), array([4]), array([4]))
>>> id(a), id(b), id(c)
(4692042016, 4692042016, 4692042016)
>>> c = c + 1
>>> a, b, c
(array([4]), array([4]), array([5]))
>>> id(a), id(b), id(c)
(4692042016, 4692042016, 4692041936)

なお、属性の直接の変更は、下記のように同一オブジェクトを参照している場合は、どちらも変わって見える。
インプレースかどうかの問題ではなく、オブジェクトと参照の問題であるはず。(言葉足らず)

>>> class Obj: pass
...
>>> a = Obj()
>>> a.name = 'mori'
>>> b = a
>>> a.name, b.name
('mori', 'mori')
>>> a.name += 'ta'
>>> a.name, b.name
('morita', 'morita')
>>> a.name = a.name + 'rou'
>>> a.name, b.name
('moritarou', 'moritarou')

参考にさせていただいた本

『ゼロから作るDeep Learning③ フレームワーク編』斎藤康毅著 O'REILLY

感想

numpyの配列を使う際に、気を付けようと思った。

今後

代入演算子に注意。