配列ビューとシャローコピー ― 修正の伝播に注意
このトピックを終えると
NumPyにおけるビュー(view)とコピー(copy)の違いを理解し、スライシングがビューを返す理由を説明でき、意図しない修正の伝播を防ぐことができるようになります。
驚くべき動作
import numpy as np
original = np.array([1, 2, 3, 4, 5])sliced = original[1:4]
sliced[0] = 99
print(sliced) # [99 3 4]print(original) # [ 1 99 3 4 5] ← 元の配列も変わる!slicedのみを変更したのに、originalも変わりました。これはバグではなく、設計によるものです。NumPyのスライシングはデータをコピーせず、**同じメモリを共有するビュー(view)**を返します。
ビュー(View)とは
ビューは、同じデータを別の視点から見るものです。
メモリ: [1] [2] [3] [4] [5]
↑ ↑ ↑ ↑ ↑
original: [0] [1] [2] [3] [4]
sliced = original[1:4]
↑ ↑ ↑
sliced: [0] [1] [2] ← 同じメモリを指す!データがコピーされないため:
- メモリ節約: 1GBの配列のスライスでも追加のメモリは不要
- 高速: コピーにかかる時間が0
- 修正の伝播: ビューを変更すると、元の配列も変わる(注意!)
ビューが作成される場合
arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])
# 1. スライシング → ビューv1 = arr[2:5] # ビューv1.base is arr # True (arrのビューである)
# 2. reshape → ビュー(可能な場合)v2 = arr.reshape(3, 3) # ビューv2.base is arr # True
# 3. 転置 → ビューmat = np.array([[1, 2], [3, 4]])v3 = mat.T # ビューv3.base is mat # True
# 4. dtypeの変換(同じサイズ) → ビューv4 = arr.view(np.int64) # ビュービューかどうかを確認する方法
arr = np.array([1, 2, 3, 4, 5])sliced = arr[1:4]copied = arr[1:4].copy()
print(sliced.base is arr) # True — ビューprint(copied.base is None) # True — 独立したコピー(baseなし)baseがNoneの場合、それはコピーであり、そうでなければ別の配列のビューです。
コピーが作成される場合
arr = np.array([1, 2, 3, 4, 5])
# 1. ファンシーインデックス → コピーc1 = arr[[0, 2, 4]] # コピー!c1[0] = 99print(arr) # [1 2 3 4 5] — 元の配列は変更されない
# 2. ブールインデックス → コピーc2 = arr[arr > 3] # コピー!c2[0] = 99print(arr) # [1 2 3 4 5] — 元の配列は変更されない
# 3. 明示的なコピーc3 = arr.copy() # コピーc3[0] = 99print(arr) # [1 2 3 4 5] — 元の配列は変更されない| 演算 | 結果 |
|---|---|
arr[2:5] (スライシング) | ビュー |
arr[[0,2,4]] (ファンシーインデックス) | コピー |
arr[arr > 3] (ブールインデックス) | コピー |
arr.reshape(...) | ビュー (可能な場合) |
arr.copy() | コピー |
arr.flatten() | コピー |
arr.ravel() | ビュー (可能な場合) |
pandasとの違い
import pandas as pd
df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]})
# pandasでのスライシングsubset = df[df["A"] > 1]subset["B"] = 99# SettingWithCopyWarningが発生する可能性あり!pandasは、ビュー/コピーの動作が状況によって異なるため、予測が難しい場合があります。そのため、SettingWithCopyWarningが存在します。安全な方法:
# 確実なコピーsubset = df[df["A"] > 1].copy()subset["B"] = 99 # 元の配列は変更されない、警告なし
# 元の配列の変更が目的の場合df.loc[df["A"] > 1, "B"] = 99 # 直接変更実践でよくある間違い
関数内で元の配列を修正
def normalize(data): # Bad: dataがビューの場合、元の配列が修正される data -= data.mean() return data
original = np.array([10.0, 20.0, 30.0])result = normalize(original[0:3]) # スライシング = ビュー!print(original) # [−10. 0. 10.] ← 元の配列が変更される!
# Good: コピーから作業def normalize_safe(data): result = data.copy() result -= result.mean() return result大容量データでのビューの活用
# 10GBのデータの一部のみを分析huge_data = np.memmap("data.bin", dtype=np.float64, shape=(1_000_000_000,))
# ビュー:追加のメモリは不要chunk = huge_data[1000:2000] # ビュー — メモリコピーなし
# コピー:メモリの割り当てchunk_copy = huge_data[1000:2000].copy() # コピー — 8KB割り当て大容量データでは、ビューを意図的に活用してメモリを節約します。修正が必要な場合にのみ、copy()を呼び出します。
Pythonリストとの比較
Pythonリストのスライシングは、NumPyとは異なり、常にコピーを返します。
# Pythonリスト:スライシング = 常にシャローコピーpy_list = [1, 2, 3, 4, 5]sliced = py_list[1:4]sliced[0] = 99print(py_list) # [1, 2, 3, 4, 5] — 元の配列は変更されない!
# NumPy:スライシング = ビュー(共有)np_arr = np.array([1, 2, 3, 4, 5])sliced = np_arr[1:4]sliced[0] = 99print(np_arr) # [ 1 99 3 4 5] — 元の配列が変更される!この違いを理解していないと、NumPyに移行した際に深刻なバグが発生する可能性があります。Pythonリストでは安全だったコードが、NumPyでは元の配列を破壊する可能性があります。
シャローコピー vs ディープコピー
import copy
nested = [[1, 2], [3, 4]]
# シャローコピー:外部リストのみコピー、内部リストは共有shallow = copy.copy(nested)shallow[0][0] = 99print(nested) # [[99, 2], [3, 4]] — 内部リストが変更される!
# ディープコピー:すべてのネストされたオブジェクトまでコピーnested = [[1, 2], [3, 4]]deep = copy.deepcopy(nested)deep[0][0] = 99print(nested) # [[1, 2], [3, 4]] — 元の配列は変更されないNumPyの.copy()は、データ全体をコピーするため、ディープコピーと同じ効果があります。NumPy配列は内部に他のPythonオブジェクトを含まない(純粋な数値データであるため)、シャロー/ディープコピーの区別は意味がなく、.copy()だけで十分です。
判断フローチャート
配列演算の結果が必要か?
├── 元の配列を変更してもよいか?
│ ├── Yes → ビューを使用(スライシングのまま)
│ └── No → .copy()を呼び出す
└── メモリが十分か?
├── Yes → .copy()で安全に
└── No → ビューを使用、変更に注意重要なまとめ
| 概念 | まとめ |
|---|---|
| ビュー(View) | 同じメモリを共有。変更すると元の配列に伝播 |
| コピー(Copy) | 独立したメモリ。変更しても元の配列は変更されない |
| スライシング | ビューを返す(NumPy) |
| ファンシー/ブールインデックス | コピーを返す |
.base | Noneであればコピー、そうでなければビュー |
.copy() | 明示的なディープコピー |
NumPyのビューは、パフォーマンス最適化のための設計です。数GBのデータをコピーせずに扱えるようにしますが、「変更すると元の配列も変わる」という副作用を伴います。ルールは単純です:スライシング = ビュー、ファンシー/ブール = コピー。 確信がない場合は.copy()。
pandas 2.0以降、Copy-on-Write(CoW)モードが導入され、スライシングの結果を変更すると自動的にコピーが行われます。pd.options.mode.copy_on_write = Trueで有効にでき、今後のデフォルトの動作になる予定です。ただし、NumPyは依然として明示的なビュー/コピーモデルに従うため、このトピックのルールを確実に理解する必要があります。