NumPy配列ndarrayのビューとコピー(メモリの共有)
NumPy配列numpy.ndarrayにおけるビュー(view)とコピー(copy)について説明する。
ndarrayのコピーを生成するにはcopy()メソッド、ndarrayが他のndarrayのビューかコピーかを判定するにはbase属性、2つのndarrayがメモリを共有しているかを判定するにはnp.shares_memory()やnp.may_share_memory()関数を使う。
pandas.DataFrameにおけるビューとコピーについては以下の記事を参照。
本記事のサンプルコードのNumPyのバージョンは以下の通り。バージョンによって仕様が異なる可能性があるので注意。
import numpy as np
print(np.__version__)
# 1.26.1
NumPy配列ndarrayのビューとコピー
NumPy配列ndarrayにはビュー(view)とコピー(copy)がある。
ndarrayから別のndarrayを生成するとき、元のndarrayとメモリを共有する(元のndarrayのメモリの一部または全部を参照する)ndarrayをビュー、元のndarrayと別にメモリを新たに確保するndarrayをコピーという。
ビューを生成する例
例えば、スライスはビューを生成する。
a = np.arange(6).reshape(2, 3)
print(a)
# [[0 1 2]
# [3 4 5]]
a_slice = a[:, :2]
print(a_slice)
# [[0 1]
# [3 4]]
同じメモリを参照しているので、一方のオブジェクトの要素の値を変更すると他方の値も変更される。
a_slice[0, 0] = 100
print(a_slice)
# [[100 1]
# [ 3 4]]
print(a)
# [[100 1 2]
# [ 3 4 5]]
a[0, 0] = 0
print(a)
# [[0 1 2]
# [3 4 5]]
print(a_slice)
# [[0 1]
# [3 4]]
スライスだけでなく、reshape()など、関数・メソッドにもビューを返すものがある。
コピーを生成する例
ブーリアンインデックスやファンシーインデックスはコピーを生成する。
a = np.arange(6).reshape(2, 3)
print(a)
# [[0 1 2]
# [3 4 5]]
a_boolean_index = a[:, [True, False, True]]
print(a_boolean_index)
# [[0 2]
# [3 5]]
メモリを共有していないので、一方のオブジェクトの要素の値を変更しても他方の値は変更されない。
a_boolean_index[0, 0] = 100
print(a_boolean_index)
# [[100 2]
# [ 3 5]]
print(a)
# [[0 1 2]
# [3 4 5]]
NumPy配列ndarrayのコピーを生成: copy()
ndarrayのコピーを生成するにはcopy()メソッドを使う。ビューからコピーを生成することも可能。
a = np.arange(6).reshape(2, 3)
print(a)
# [[0 1 2]
# [3 4 5]]
a_slice_copy = a[:, :2].copy()
print(a_slice_copy)
# [[0 1]
# [3 4]]
一方のオブジェクトの要素の値を変更しても他方の値は変更されない。例えば、スライスで選択した部分配列を元の配列とは別々に処理したい場合はcopy()を使えばよい。
a_slice_copy[0, 0] = 100
print(a_slice_copy)
# [[100 1]
# [ 3 4]]
print(a)
# [[0 1 2]
# [3 4 5]]
なお、view()というメソッドもあるが、これはあくまでも呼び出し元のビューを生成するもの。
ブーリアンインデックスやファンシーインデックスで生成したオブジェクトからview()を実行してもコピーのビューが生成されるだけで、大元のオブジェクトのビューが生成されるわけではない。
a_boolean_index_view = a[:, [True, False, True]].view()
print(a_boolean_index_view)
# [[0 2]
# [3 5]]
a_boolean_index_view[0, 0] = 100
print(a_boolean_index_view)
# [[100 2]
# [ 3 5]]
print(a)
# [[0 1 2]
# [3 4 5]]
ビューかコピーか判定: base属性
ndarrayがビューかコピーか(厳密にはビューかそうでないか)を判定するにはbase属性を使う。
ndarrayがビューである場合、base属性はオリジナルのndarrayを示す。
スライスとreshape()を例とする。形状を変更するreshape()は可能な限りビューを返す。
a = np.arange(10)
print(a)
# [0 1 2 3 4 5 6 7 8 9]
a_0 = a[:6]
print(a_0)
# [0 1 2 3 4 5]
print(a_0.base)
# [0 1 2 3 4 5 6 7 8 9]
a_1 = a_0.reshape(2, 3)
print(a_1)
# [[0 1 2]
# [3 4 5]]
print(a_1.base)
# [0 1 2 3 4 5 6 7 8 9]
新たに生成したndarrayや、コピーのbase属性はNone。
a = np.arange(10)
print(a)
# [0 1 2 3 4 5 6 7 8 9]
print(a.base)
# None
a_copy = a.copy()
print(a_copy)
# [0 1 2 3 4 5 6 7 8 9]
print(a_copy.base)
# None
base属性がNoneでないとビューであると判定できる。Noneとの比較にはis演算子を使う。
- 関連記事: PythonにおけるNoneの判定
print(a_0.base is None)
# False
print(a_copy.base is None)
# True
print(a.base is None)
# True
元のndarrayとの比較や、ビューのbase同士の比較で、メモリを共有していることも確認できる。
print(a_0.base is a)
# True
print(a_0.base is a_1.base)
# True
メモリを共有しているかどうかの判定は次に説明するnp.shares_memory()のほうが便利。
メモリを共有しているか判定: np.shares_memory()
2つのndarrayがメモリを共有しているかはnp.shares_memory()関数で判定できる。
基本的な使い方
np.shares_memory()に判定したい2つのndarrayを指定する。メモリを共有しているとTrueが返される。
a = np.arange(6)
print(a)
# [0 1 2 3 4 5]
a_reshape = a.reshape(2, 3)
print(a_reshape)
# [[0 1 2]
# [3 4 5]]
print(np.shares_memory(a, a_reshape))
# True
共通のndarrayから生成されたビュー同士でもTrueとなる。
a_slice = a[2:5]
print(a_slice)
# [2 3 4]
print(np.shares_memory(a_reshape, a_slice))
# True
コピーの場合はFalse。
a_reshape_copy = a.reshape(2, 3).copy()
print(a_reshape_copy)
# [[0 1 2]
# [3 4 5]]
print(np.shares_memory(a, a_reshape_copy))
# False
np.may_share_memory()との違い
np.may_share_memory()という関数もある。
- numpy.may_share_memory — NumPy v1.26 Manual
- python - What is the difference between numpy.shares_memory and numpy.may_share_memory? - Stack Overflow
関数名にmayが含まれていることからも分かるように、np.may_share_memory()はnp.shares_memory()に比べて厳密ではない。
np.may_share_memory()はメモリアドレスの範囲がオーバーラップしているかどうかを判定するのみで、同じメモリを参照している要素があるかどうかは考慮しない。
例えば以下のような場合。2つのスライスは同じndarrayのビューでオーバーラップした範囲を参照しているが、1個おきであるためそれぞれの要素自体は別々のメモリを参照している。
a = np.arange(10)
print(a)
# [0 1 2 3 4 5 6 7 8 9]
a_0 = a[::2]
print(a_0)
# [0 2 4 6 8]
a_1 = a[1::2]
print(a_1)
# [1 3 5 7 9]
np.shares_memory()は厳密に判定するためFalseを返すが、np.may_share_memory()はTrueとなる。
print(np.shares_memory(a_0, a_1))
# False
print(np.may_share_memory(a_0, a_1))
# True
以下の例では、2つのスライスが元のndarrayの前半と後半で範囲が重なっていないためnp.may_share_memory()でもFalseとなる。
a_2 = a[:5]
print(a_2)
# [0 1 2 3 4]
a_3 = a[5:]
print(a_3)
# [5 6 7 8 9]
print(np.shares_memory(a_2, a_3))
# False
print(np.may_share_memory(a_2, a_3))
# False
厳密な判定であるnp.shares_memory()のほうが処理時間は長い。以下のコードはJupyter Notebookのマジックコマンド%%timeitを利用しており、Pythonスクリプトとして実行しても計測されないので注意。
%%timeit
np.shares_memory(a_0, a_1)
# 200 ns ± 1.1 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
%%timeit
np.may_share_memory(a_0, a_1)
# 123 ns ± 0.284 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
上の例ではそこまでの差はないが、np.shares_memory()は入力によっては指数関数的に遅くなることが警告されている。
Warning
This function can be exponentially slow for some inputs, unless max_work is set to a finite number or MAY_SHARE_BOUNDS. If in doubt, use numpy.may_share_memory instead. numpy.shares_memory — NumPy v1.26 Manual
np.may_share_memory()は各要素がメモリを共有していない場合に誤ってTrueを返す可能性があるが、メモリを共有しているのに誤ってFalseを返すことはない。メモリを共有している可能性があるかの判定で問題ないのであればnp.may_share_memory()を使うとよい。