NumPy

【NumPy】「NumPy配列(ndarray)のスライス」と「Python組み込みのlistのスライス」の違い

【NumPy】「NumPy配列(ndarray)のスライス」と「Python組み込みのlistのスライス」の違い

「NumPy配列(ndarray)のスライス」と「Python組み込みのlistのスライス」の違いについて解説します。

「NumPy配列(ndarray)のスライス」と「Python組み込みのlistのスライス」の違い

NumPyの配列(ndarray)はPythonのリストのスライスと同様にスライスにより一部分を取り出すことができます。しかし、NumPyの配列とPython組み込みのlistではスライスの意味が少し異なります。

結論から述べると「NumPyの配列(ndarray)のスライスはビューなので、スライスした値を変更すると元の配列のデータも変更されるが、Python組み込みのlistのスライスはコピーなのでスライスした値を変更しても元の配列のデータは変更されない」ということになります。

以降でそれぞれの例を見ながら確認をしてみます。

NumPy配列(ndarray)のスライスはビュー

NumPyの配列をスライスで取得した場合には、ビューになるためスライスで取得したデータを変更すると元のデータも変更されます。1次元配列の場合と2次元配列の場合で簡単な例を見てみましょう。

【1次元配列で確認】

import numpy as np

# ===== 1次元配列で確認
data_1d = np.arange(10)
print(f'元のデータ: {data_1d}')

# スライスで一部のデータを取得する
tmp_data_1d = data_1d[3:8]
print(f'スライスデータ: {tmp_data_1d}')
# スライスのデータを変更する
print('\n=== スライスしたデータを変更')
tmp_data_1d[:] = 0
print(f'スライスデータ: {tmp_data_1d}')
# 元のデータを確認する
print(f'元のデータ: {data_1d}')
【実行結果】
元のデータ: [0 1 2 3 4 5 6 7 8 9]
スライスデータ: [3 4 5 6 7]

=== スライスしたデータを変更
スライスデータ: [0 0 0 0 0]
元のデータ: [0 1 2 0 0 0 0 0 8 9]

【2次元配列で確認】

import numpy as np

# ===== 2次元配列で確認
data_2d = np.arange(25).reshape((5, 5))
print(f'元のデータ: \n{data_2d}')

# スライスで一部のデータを取得する
tmp_data_2d = data_2d[1:4, 1:4]
print(f'スライスデータ: \n{tmp_data_2d}')
# スライスのデータを変更する
print('\n=== スライスしたデータを変更')
tmp_data_2d[:, :] = 0
print(f'スライスデータ: \n{tmp_data_2d}')
# 元のデータを確認する
print(f'元のデータ: \n{data_2d}')
【実行結果】
元のデータ: 
[[ 0  1  2  3  4]
 [ 5  6  7  8  9]
 [10 11 12 13 14]
 [15 16 17 18 19]
 [20 21 22 23 24]]
スライスデータ: 
[[ 6  7  8]
 [11 12 13]
 [16 17 18]]

=== スライスしたデータを変更
スライスデータ: 
[[0 0 0]
 [0 0 0]
 [0 0 0]]
元のデータ: 
[[ 0  1  2  3  4]
 [ 5  0  0  0  9]
 [10  0  0  0 14]
 [15  0  0  0 19]
 [20 21 22 23 24]]

上記の例では、tmp_data_1dやtmp_data_2dにスライスにより取得したデータを保持しています。

その値をすべて0にした後にもとのデータであるdata_1dやdata_2dを参照すると、スライスした部分の数字がすべて0に変わっていることがわかります。

2次元までで確認しましたが、3次元以上のn次元配列となっても考え方は同じです。

Python組み込みのlistのスライスはコピー

Pythonのリストの場合はスライスはコピーとなります。1次元の例だけ確認してみましょう。2次元以降の多次元配列でも考え方は同様です。

data_1d = [i for i in range(10)]
print(f'元のデータ: {data_1d}')

# スライスで一部のデータを取得する
tmp_data_1d = data_1d[3:8]
print(f'スライスデータ: {tmp_data_1d}')
# スライスのデータを変更する
print('\n=== スライスしたデータを変更')
for i, _ in enumerate(tmp_data_1d):
    tmp_data_1d[i] = 0
print(f'スライスデータ: {tmp_data_1d}')
# 元のデータを確認する
print(f'元のデータ: {data_1d}')
【実行結果】
元のデータ: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
スライスデータ: [3, 4, 5, 6, 7]

=== スライスしたデータを変更
スライスデータ: [0, 0, 0, 0, 0]
元のデータ: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Pythonのリストの場合、スライスで取得したデータの要素を変更しても値が変わっていないため、スライスがコピーをしているということがわかります。

NumPy配列(ndarray)のスライスをコピーにしたい場合

NumPy配列(ndarray)のスライスをビューではなくコピーとして利用したい場合には、明示的にコピーをしてから使用する必要があります。

NumPy配列をコピーする場合には、copy関数を使用することで配列のコピーをすることができます。

import numpy as np

data = np.arange(10)
print(f'data: {data}')

# データをコピーする
data_copy = data[3:8].copy()
print(f'data_copy: {data_copy}')

# コピーしたデータを変更する
print('\nコピーしたデータを変更する')
data_copy[:] = 0
print(f'data_copy: {data_copy}')
# もとのデータを確認する
print(f'data: {data}')
【実行結果】
data: [0 1 2 3 4 5 6 7 8 9]
data_copy: [3 4 5 6 7]

コピーしたデータを変更する
data_copy: [0 0 0 0 0]
data: [0 1 2 3 4 5 6 7 8 9]

上記の例のdata[3:8].copy()のようにスライスしたデータでcopy関数を実行することで、当該スライスのコピーを作成することができます。

コピーした変数の値を全て0に変更していますが、元のデータには変更がされていないことが分かるかと思います。

Note

上記はスライスでと書きましたが、配列をそのまま別の変数に代入した場合で、当該変数を変更したときも元のデータが変更されます。

NumPy配列の操作をする際に元のデータを変更したくない場合は、copy関数で配列をコピーしてから操作をすると覚えておきましょう。

Note

copy関数のドキュメントの記載はこちらを参照してください。