コピーと閲覧数
チャーリーはサブ配列を変更したところ、元の配列も変更されてしまっていたことに気づき、2時間もデバッグに費やした。アリスはこう言う。「.base属性を確認して。もしbase isが元の配列なら、それはビューだ。迷ったら、.copy()。」
この事例は、重要な問題を浮き彫りにしています。すなわち、NumPyの演算のすべてが独立した配列を返すわけではないということです。微妙なデータ改変バグを回避するためには、「ビュー」と「コピー」の違いを理解することが不可欠です。
1. ビューの仕組み
(1) ビューとは何か
ビューは、元の配列へのもうひとつの「窓」であり、同じメモリを共有しています。ビューを変更すると、元の配列が直接変更されます。
import numpy as np
a = np.array([1, 2, 3, 4, 5])
b = a[1:4] # slice returns a view
b[0] = 999
print(a) # [ 1 999 3 4 5] original changed!
print(b) # [999 3 4]
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
ビューの利点はゼロコピーである――余分なメモリを消費せず、極めて高速だ。
(2) 記憶上の関係
a = np.array([10, 20, 30])
b = a[:] # view
print(a.__array_interface__['data'][0]) # original data address
print(b.__array_interface__['data'][0]) # view data address (same!)
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
どちらも同じメモリの開始アドレスを指しており、異なるのはメタデータ(形状、ストライド)のみです。
(3) .base 属性
a = np.arange(12).reshape(3, 4)
b = a[0:2, 1:3] # slice view
c = a[[0, 2]] # fancy indexing — copy
print(b.base is a) # True — view
print(c.base is a) # False — copy
2. ビューを返す操作とコピーを返す操作の違い
(1) ビュー(共有メモリ、コピーなし)
| 操作 | 例 |
|---|---|
| 基本的なスライシング | a[1:5], a[1:5:2], a[:, 2:4] |
.view() |
a.view(np.float32) |
reshape |
a.reshape(3, 4) |
transpose / .T |
a.T, a.transpose(1, 0) |
ravel (通常) |
a.ravel() |
squeeze |
a.squeeze() |
newaxis |
a[:, np.newaxis] |
(2) コピー(新しいメモリ、独立)
| 操作 | 例 |
|---|---|
.copy() |
a.copy() |
| 凝った索引 | a[[0, 2, 4]] |
| ブール索引 | a[a > 0] |
astype |
a.astype(np.float32) |
flatten |
a.flatten() |
np.where |
np.where(a > 0, a, 0) |
(1) ▶ サンプル
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
: .base を使って「view」と「copy」を比較する(難易度 ⭐)
import numpy as np
a = np.arange(12).reshape(3, 4)
# Basic slice — view
b = a[0:2]
print(f"slice base is a: {b.base is a}") # True
# Reshape — view
c = a.reshape(6, 2)
print(f"reshape base is a: {c.base is a}") # True
# Fancy indexing — copy
d = a[[0, 2]]
print(f"fancy base is a: {d.base is a}") # False
# Boolean indexing — copy
e = a[a > 5]
print(f"boolean base is a: {e.base is a}") # False
> Output: Run NumPy 2.x in your local Python environment to see the ndarray output. The Piston server does not have NumPy pre-installed — install it locally (`pip install numpy`) and compare. Actual values may vary by NumPy version and random seed.
3. 比較表
(1) 「表示」と「コピー」の決定木
Is it a basic slice (start:stop:step)?
├── Yes → View (shared memory)
└── No → Is it fancy/boolean indexing?
├── Yes → Copy (new memory)
└── No → Check .base attribute
```text
### (2) パフォーマンスへの影響
| 演算 | 時間 (100万要素) | メモリ |
|-----------|-------------------|--------|
| ビュー(スライス) | O(1) | 共有 |
| コピー(高度な) | O(n) | 新規割り当て |
| .copy() | O(n) | 新規割り当て |
| 型 | O(n) | 新規割り当て |
### (1) ▶ サンプル:ビューの形状変更と転置(難易度 ⭐⭐)
```python
import numpy as np
a = np.arange(12).reshape(3, 4)
# Reshape returns a view
b = a.reshape(6, 2)
print(f"reshape base is a: {b.base is a}")
# Transpose returns a view
c = a.T
print(f"transpose base is a: {c.base is a}")
# Modifying the view changes the original
c[0, 0] = 999
print(f"a[0, 0] after transpose mod: {a[0, 0]}")
出力:
TEXTreshape base is a: True transpose base は a: True a[0, 0] を転置した後、mod 999 で計算すると:999
(2) ▶ サンプル:flatten 対 ravel — コピー 対 ビュー (難易度 ⭐⭐)
import numpy as np
a = np.arange(12).reshape(3, 4)
# ravel usually returns a view
r = a.ravel()
print(f"ravel base is a: {r.base is a}")
# flatten always returns a copy
f = a.flatten()
print(f"flatten base is a: {f.base is a}")
# After transpose, ravel may need to copy
t = a.T
r2 = t.ravel()
print(f"ravel of transposed base is a: {r2.base is a}")
出力:
TEXTravel base は a です: True flatten base は a: False です 塩基の転位に関するラベルは a:False
❓ よくある質問
arr.base is original を確認してください。True の場合はビューです。arr.base が None の場合は、その配列は独自のデータを保持しています。a.view(np.float32) を使用すると、同じバイト列を別のデータ型として再解釈します。これは危険であり、必要となることはほとんどありません。変換を伴う安全なコピーを行うには、astype を使用してください。📖 まとめ
- ビューは元の配列とメモリを共有しているため、ビューを変更すると元の配列も変更されます
arr.base is original— True は「表示」を意味し、False は「独立」を意味する- 基本的なスライシング、リシェイプ、トランスポーズ、ラヴェル:ビューを返す
- ファンシーインデックス、ブールインデックス、flatten、astype:コピーを返す
- ビューは高速(O(1))だが危険である。一方、コピーは安全だがメモリを消費する。
- 迷った場合は、明示的に
.copy()を使用してください
📝 練習問題
-
初心者向け(難易度 ⭐):10要素の1次元配列を作成してください。スライス、特殊なインデックス、ブール値によるマスク選択を作成してください。それぞれについて、結果を変更した際に元の値が変更されるかどうかを確認してください。
-
中級(難易度 ⭐⭐):3×4の配列を作成し、その中から2×2のスライスを取り出して変更を加えます。元の配列が変化する様子を確認してください。次に、
.copy()を使用して同じ操作を繰り返してください。元の配列が変化しないことを確認してください。 -
上級(難易度 ⭐⭐⭐):3次元配列を作成し、それを転置してから、その結果の形状を変更してみてください。形状変更によってビューが返されるかどうかを確認してください(
.baseを使用してください)。返される理由、または返されない理由を説明してください。