配列を切り出したあとに元の値まで変わるのは、切り出した配列が同じデータ領域を参照するviewだからである。原本を保存したいならcopy()を明示する。ただし、NumPyでは切り出し方によってviewとcopyが変わるため、「別の変数名へ代入したから独立した」と考えない方がよい。
最小例で確かめる
以下は説明用に作った小さなデータである。実測データや実行速度の測定結果ではない。コード全体をexample.pyとして保存すれば、入力ファイルを別途用意せずに実行できる。assertは、この例で成り立つべき形や値を確認するために入れてある。
import numpy as np
original = np.arange(6, dtype=np.int64)
view = original[1:4]
independent = original[1:4].copy()
selected = original[[1, 3]]
view[0] = 99
selected[0] = -5
print("original:", original.tolist())
print("copy:", independent.tolist())
print("advanced index:", selected.tolist())
print("shares:", np.shares_memory(original, view),
np.shares_memory(original, independent),
np.shares_memory(original, selected))
original[[1, 3]] = [7, 8] # 元配列を更新する代入
print("direct assignment:", original.tolist())
np.testing.assert_array_equal(original, [0, 7, 2, 8, 4, 5])
np.testing.assert_array_equal(independent, [1, 2, 3])
np.testing.assert_array_equal(selected, [-5, 3])
assert np.shares_memory(original, view)
assert not np.shares_memory(original, independent)
実行結果
original: [0, 99, 2, 3, 4, 5]
copy: [1, 2, 3]
advanced index: [-5, 3]
shares: True False False
direct assignment: [0, 7, 2, 8, 4, 5]
スライスはデータを共有する
original[1:4]は基本的なスライスであり、通常の数値ndarrayでは元のデータを参照するviewを作る。view[0]はoriginal[1]と同じ場所を表すため、99を書き込むと原本も変わる。配列のshapeや開始位置は別々でも、実際の要素が保存されたメモリは共通なのである。
一方、copy()で作ったindependentは独立したデータを持つ。コピーした時点の[1, 2, 3]が保存され、その後にoriginalやviewを変更しても追随しない。補正前の値を後で比較したいなら、補正する前にコピーしておく必要がある。変更後にコピーしても変更前の値は取り戻せない。
高度なインデックスと代入を区別する
original[[1, 3]]のように整数配列で選ぶ高度なインデックスは、読み出した結果としてcopyを返す。selected[0]を書き換えてもoriginalには戻らない。booleanマスクによる読み出しも同じくcopyになる。見かけ上は同じ「一部を取り出す」処理でも、コロンによるスライスとは性質が違う。
ただし、original[[1, 3]] = [7, 8]は元配列への代入である。右辺へ読み出してからその別配列を編集する操作ではなく、元配列の指定位置を書き換える。高度なインデックスがcopyを返すという説明を、代入の左辺にもそのまま当てはめると混乱する。読み出しと更新を分けて考えよう。
共有状態を調べてから処理を設計する
np.shares_memoryは二つの配列が実際にメモリを共有するか調べる関数である。例ではviewだけがTrueになり、値の変化と一致する。複雑なstrideを持つ配列では厳密な共有判定に計算量が掛かることがあるので、日常の処理で無制限に呼ぶより、問題の切り分けや小さな検査に使うとよい。
配列のbase属性は手掛かりにはなるが、直前の変数そのものを常に指すとは限らない。何度もreshapeやスライスを経由すると参照関係が直感とずれる。base is originalだけで独立性を断定せず、処理の契約として「入力を変更しない」「返した配列を変更してよい」などを決める方が読みやすい。
関数の中で入力を補正するなら、入口でcopy()して以降その配列を使う方法が分かりやすい。ただし巨大配列を毎回コピーすればメモリを多く使う。原本の保持が必要な箇所に限定する、変更する列だけ別に持つなど、必要な独立性と保存量をセットで考える。
この説明は数値dtypeの要素についての話である。dtype=objectに入れたリストなどは、配列をcopy()しても中のPythonオブジェクトまで再帰的に複製されない。また、読み取り専用のviewを作っただけでは別の参照から元データを変更することまでは防げない。共有の仕組みとアクセス制限も区別しておきたい。
動作確認環境と参考資料
Linux・CPython 3.12.14、NumPy 2.3.5、pandas 2.2.3、SciPy 1.17.0、Matplotlib 3.10.8の環境で掲載コードを実行した。使用するライブラリはコード冒頭のimportを参照してほしい。公式資料の最新版と、この実行確認版は区別している。数値の末尾や表の表示幅は環境によって変わることがある。
