おっぱいそん!

pythonを数値計算に使うときのテクニックとかをまとめていきたい。

Pythonの代入の挙動

Pythonで代入をした時に、各変数がどのオブジェクトを指すかをまとめておく。特に、list(リスト)とNdarray(配列)の違いをば。
前回の記事も参照 : http://oppython.hatenablog.com/entry/2015/01/15/014729

例1

前回の記事でも書いたようにPythonでは、代入(=)は変数に値(=オブジェクト)を格納しているのではなく(値渡し)、値(=オブジェクト)への参照を格納している(参照渡し)。

x = np.array([1])
y = x
print 'id(x)_before =', id(x)
print 'id(y)_berore =',  id(y)
x = np.array([2])
print 'id(x)_after =', id(x)
print 'id(y)_after =',  id(y)
print y

id(x)_before = 4300100832
id(y)_berore = 4300100832
id(x)_after = 4300101152
id(y)_after = 4300100832
[1]

y = xとすると、変数yもxと同じオブジェクトnp.array([1])を指すようになる(参照渡し)。
変数xに新しいオブジェクトnp.array([2])を代入すると、xは新しいオブジェクトを参照するようになる。
もちろん、yには何もしていないので、始めのオブジェクトを参照したままである。
上の挙動は、ndarrayだけでなく、タプル、文字列、リスト、数値などでも共通(型が変更可能かどうかに依らない)。

例2

一方、以下のコードでは、xを書き換えると、yも書き換わっている。

x = np.array([1])
y = x
print 'id(x)_before =', id(x)
print 'id(y)_berore =',  id(y)
x[:] = np.array([2])
print 'id(x)_after =', id(x)
print 'id(y)_after =',  id(y)
print y

id(x)_before = 4300100832
id(y)_berore = 4300100832
id(x)_after = 4300100832
id(y)_after = 4300100832
[2]

y = xとすると、yもxも同じオブジェクトを参照するところまでは、前のコードと同じである。
ところが、x[:] = のように書くと、左辺は変数xではなく、xが参照するオブジェクトを指すようになる。
したがって、x[:] = 2 はxの指すオブジェクト(yも同じオブジェクトを参照している)を書き換えることになるので、yも変更される。
リストも上のNdarrayと同じ振る舞いをする。
一方、タプルのような変更不可能な型の場合は、そもそもx[:] = のような代入は出来ず、書き換えようとすると以下のようなエラーがでる。:
TypeError: 'tuple' object does not support item assignment

例3

xを書き換えた時に、yを書き換えたくない場合には次のようにする。

x = [1]
y = x[:]
print 'id(x)_before =', id(x)
print 'id(y)_berore =',  id(y)
x[:] = [2]
print 'id(x)_after =', id(x)
print 'id(y)_after =',  id(y)
print y

id(x)_before = 4329648568
id(y)_berore = 4329649144
id(x)_after = 4329648568
id(y)_after = 4329649144
[1]

y = xを、y = x[:]のように書き換えると、yはxの参照するオブジェクトのコピーをとって、その新しくコピーしたオブジェクトを参照するようになる。
したがって、x[:] = [2]でxの参照するオブジェクトを書き換えても、yの参照するオブジェクトは別なので、yは変わらない。


このlistの例と同じことをNdarrayにすると、同じ結果になると思うかもしれない。
ところが!!

x = np.array([1])
y = x[:]
print 'id(x)_before =', id(x)
print 'id(y)_berore =',  id(y)
x[:] = np.array([2])
print 'id(x)_after =', id(x)
print 'id(y)_after =',  id(y)
print y

id(x)_before = 4301149408
id(y)_berore = 4301149728
id(x)_after = 4301149408
id(y)_after = 4301149728
[2]

y = x[:] とすると、確かにyはxとは別のオブジェクトになっている。
ところが、xの参照するオブジェクトを書き換えると、yはxとは別のオブジェクトを参照しているはずなのに、yも書き換わってしまう!!

これは、yはxとは別のオブジェクトを参照しているが、yのオブジェクトはxのオブジェクトのview(ビュー)になっており、同じデータを見ているためである。
このような仕様になっているのは、listと違って、Ndarrayはメモリや時間を節約することを意識して作られているためである。
このように同じデータを共有する異なるオブジェクトを作れるおかげで、例えば以下のリンクにあるように、データは同じ(メモリを共有)だが、形の違うNdarrayを作れたりする。
http://wiki.scipy.org/Tentative_NumPy_Tutorial#head-1529ae93dd5d431ffe3a1001a4ab1a394e70a5f2
http://naoyat.hatenablog.jp/entry/2011/12/29/021414
y = x[:] は y = x.view()とも書ける。
また、同じメモリ(つまり同じデータ)を共有しているかを調べたい時には、numpy.may_share_memoryやbaseや'OWNDATA'を使って調べられる。
https://scipy-lectures.github.io/intro/numpy/array_object.html#copies-and-views
http://stackoverflow.com/questions/11524664/how-can-i-tell-if-numpy-creates-a-view-or-a-copy


Ndarrayを使って、xを書き換えた時に、yを書き換えたくない場合は以下のようにCopyを使う。

x = np.array([1])
y = np.copy(x) #x.copy()
print 'id(x)_before =', id(x)
print 'id(y)_berore =',  id(y)
x[:] = np.array([2])
print 'id(x)_after =', id(x)
print 'id(y)_after =',  id(y)
print y
print np.may_share_memory(x,y)

id(x)_before = 4302197984
id(y)_berore = 4302198304
id(x)_after = 4302197984
id(y)_after = 4302198304
[1]
False

関数Copyを使うと、xのオブジェクトをコピーしてyのオブジェクトを作る。
その際、yはxのViewでなく、メモリも別のオブジェクトになっているので、xを書き換えてもyは書き換えられない。


Numpyでは関数によってViewを返すのかコピーを返すのか違うので意識した方がいい。
例えば、np.reshapeやnp.transposeはViewを返し、Fancy Indexingはコピーを返す。:

a = np.arange(10)
b = np.reshape(a,(2,5))
c = a[a<5]
a[0] = 99
print np.may_share_memory(a,b)
print b
print np.may_share_memory(a,c)
print c

True
[[99 1 2 3 4]
[ 5 6 7 8 9]]
False
[0 1 2 3 4]

http://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html
http://scipy-lectures.github.io/intro/numpy/numpy.html
http://www.slideshare.net/lucidfrontier45/numpy-intro

他にも、
b += 5 と b = b + 5は前者がViewなのに対し、後者が新しいArrayを作るなど、違いがあったりするので注意が必要。
http://stackoverflow.com/questions/18155972/unexpected-result-in-numpy-array-slicing-view-vs-copy