6
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Numpy配列のスライスで、変数のスワップ(交換)する時にハマった話

Last updated at Posted at 2018-10-25

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()しよう

以上です。思わぬ所に落とし穴があって勉強になりました。

6
8
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?