view と copy
NumPy の配列 (ndarray) を変数に代入したとき、一見すると奇妙なことが起こります。
# PYTHON_NUMPY_VIEW_COPY
# In[1]
import numpy as np
a = np.array([1, 2, 3])
b = a
# 配列bの第1要素を100に変更
b[0] = 100
print("a : {}".format(a))
print("b : {}".format(b))
a : [100 2 3] b : [100 2 3]
このコードでは変数 b に配列 a を代入し、b の第 1 要素を 100 に変更しています。ところが実行結果をみると、元の配列 a の内容まで書き換えられています。これは配列の変数への代入が参照代入であることを意味しています。すなわち、b = a のように記述すると、配列 b は配列 a と同じメモリを参照するようになります。配列 b を操作するということは、a と b が共通して参照しているメモリを書き換えていることになるのです。このとき「 b は a の view (ビュー) である」といいます。
a と同じ要素をもちながら、異なるメモリを参照する配列 b を生成したいときには、copy()メソッドを用いて配列 a の copy (コピー) を作成します。
# In[2]
a = np.array([1, 2, 3])
# 配列aのコピーを作成
b = a.copy()
# 配列bの第1要素を100に変更
b[0] = 100
print("a : {}".format(a))
print("b : {}".format(b))
a : [1 2 3] b : [100 2 3]
a と b は互いに独立した別個の配列なので、この場合は b を変更しても a には反映されません。
copy を作るということは、その分だけ余分なメモリを消費するということなので、メモリ効率という観点から見ればなるべく避けたいところです。しかし最初のコードに見たように、view には書き換えのリスクが伴います。安全性を重視する場合は copy, メモリの節約が優先される場合は view というように使い分ける必要があります。
メモリアドレス
オブジェクトに割り当てられているメモリアドレスを確認したいときは id() 関数を使います。
# PYTHON_NUMPY_MEMORY_ID
# In[1]
import numpy as np
# 1次元配列を作成
a = np.array([1, 2, 3])
# 配列aのメモリアドレス
print(id(a))
63896056
もちろん、実行結果はその時々で割り当てられたアドレスにより異なります。念のために b = a として、a と b の参照先が同じであるかどうかを確認してみましょう。
# In[2]
# 1次元配列を作成
a = np.array([1, 2, 3])
b = a
# aとbのアドレスは同じ?
print(id(a) == id(b))
True
a と b は確かに同じメモリアドレスを共有しています。
要素の参照
配列 x に対して x[0] という記述は、メモリブロックへの直接的アクセスです。
# PYTHON_NUMPY_BLOCKS_MEMORY
# In[1]
import numpy as np
# 1次元配列を作成
x = np.array([1, 2, 3])
# 配列xの第1要素を参照
print(x[0])
1
すなわち、x[0] に対する操作はメモリの書き換えを意味します。
# In[2]
# 1次元配列を作成
x = np.array([1, 2, 3])
# 配列xの第1要素を100に変更
x[0] = 100
print(x)
[100 2 3]
Python におけるリストのスライシングが copy を作成するのに対して、NumPy の配列のスライシングは view を返します。したがって、スライシングによって取り出した部分配列への操作は元の配列に反映されます。
# In[3]
# 3×3の2次元配列を定義
x = np.array([[82, 20, 47],
[11, 62, 23],
[34, 79, 58]])
# スライシングによる部分配列の取り出し
y = x[:2, :2]
print("部分配列y")
print(y, \n)
# 部分配列yの変更
y[0, 0] = 100
print("配列x")
print(x)
部分配列y [[82 20] [11 62]] 配列x [[100 20 47] [11 62 23] [34 79 58]]
コメントを書く