Pythonには変数のスワップ(交換)を簡潔に書けるコードがありますが、Numpy配列をスライスしたもの同士をスワップさせようとしたときにハマったのでメモしておきます。
環境:Numpy1.15.1、Python3.6.4
普通の変数のスワップはOK
普通の変数のスワップはもちろんOKです。一時的な変数を用意しなくていいのがとても良いですね。
>>> a = 1
>>> b = 2
>>> a, b = b, a
>>> print(a, b)
2 1
aとbの値が入れ替わっていますね。
Numpyのインデックスを指定してのスワップもOK
Numpy配列のスワップが全部ダメというわけではなく、インデックスを値で指定すればスワップできます。
>>> a = np.arange(5)
>>> print(a)
[0 1 2 3 4]
>>> a[0], a[4] = a[4], a[0]
>>> print(a)
[4 1 2 3 0]
a[0]とa[4]が入れ替わっています。
Numpy配列のスライス同士のスワップがNG
ここからが本題でこれがダメな例です。
>>> a = np.arange(16).reshape(4,4)
>>> a[0:2, 0:2], a[2:4, 2:4] = a[2:4, 2:4], a[0:2, 0:2]
>>> print(a)
[[10 11 2 3]
[14 15 6 7]
[ 8 9 10 11]
[12 13 14 15]]
えっ、右下の2x2の要素が[[0, 1], [4, 5]]に置き換わっていない??
順番を入れ替えたらどうなのか
0:2→2:4の順番で入れ替えたら2:4の値がコピーされましたが、2:4→0:2の順で入れ替えたらどうなるでしょうか?
>>> a = np.arange(16).reshape(4,4)
>>> a[2:4, 2:4], a[0:2, 0:2] = a[0:2, 0:2], a[2:4, 2:4]
>>> print(a)
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 0 1]
[12 13 4 5]]
どうも先に代入されたほうの値がコピーされるみたいですね。この場合は後に代入するa[2:4, 2:4]が、最初の代入によって置き換えられてしまったのでしょう。
原因はNumpyのスライスが値のコピーではなくビューを作っているから
StackOverFlowにありました。
This is because Numpy slices don't eagerly copy data, they create views into existing data. Copies are performed only at the point when slices are assigned to, but when swapping, the copy without an intermediate buffer destroys your data.
Numpyのドキュメントには確かにビューを作っているとあります。
All arrays generated by basic slicing are always views of the original array.
スライスはただ単にビューを作っているだけで、スライス単位でのスワップはそのビューの交換にすぎないから、元の値が置き換えられてしまうとスワップで期待された通りの結果が出ないということでしょうか。
追記:コメント欄でこの内部挙動をを詳しく検証してくださった方がいました。必見です。
解決法:2つ目のスライスを.copy()しよう
StackOverFlowの方法そのままですが、代入する側の値が置き換えられる前に.copy()してしまえばいいのです。
>>> a = np.arange(16).reshape(4,4)
>>> a[0:2, 0:2], a[2:4, 2:4] = a[2:4, 2:4], a[0:2, 0:2].copy()
>>> print(a)
[[10 11 2 3]
[14 15 6 7]
[ 8 9 0 1]
[12 13 4 5]]
>>> a = np.arange(16).reshape(4,4)
>>> a[2:4, 2:4], a[0:2, 0:2] = a[0:2, 0:2], a[2:4, 2:4].copy()
>>> print(a)
[[10 11 2 3]
[14 15 6 7]
[ 8 9 0 1]
[12 13 4 5]]
どちらでもOKです。期待された通りの結果が出ました。
補足:Pythonのリストのスライス同士のスワップは一次元ならOK
Numpyの配列ではなく、Pythonのリストの場合はどうでしょうか?
>>> # Numpyの場合(NG)
>>> a=np.arange(5)
>>> a[0:2], a[3:5] = a[3:5], a[0:2]
>>> print(a)
[3 4 2 3 4]
>>> # Pythonのリストの場合(OK)
>>> b=[0,1,2,3,4]
>>> b[0:2], b[3:5] = b[3:5], b[0:2]
>>> print(b)
[3, 4, 2, 0, 1]
Numpyの配列の場合はNGで、Pythonのリストの場合は一次元なら(追記)スライス同士をスワップさせてもOKという結果になりました。興味深いですね。
追記:一次元ならOKといったのは、リストの中にリストを入れたときにシャローコピーの関係で奇妙な挙動が起こるからです。ディープコピーではないのが注意が必要です。コメント欄で指摘してくださったshiracamusさん、ありがとうございました。感謝!(コードはコメント欄からです)
>>> a = [[1],[2],[3],[4],[5],[6]]
>>> a[:3] = a[3:]
>>> a
[[4], [5], [6], [4], [5], [6]]
>>> a[0][0] = 999
>>> a
[[999], [5], [6], [999], [5], [6]]
まとめ
「Numpyの配列のスライス同士をスワップさせるときは、代入する側の2つ目の変数を.copy()しよう」
以上です。思わぬ所に落とし穴があって勉強になりました。