NumPy配列ndarrayのビューとコピー(メモリの共有)

Modified: | Tags: Python, NumPy

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演算子を使う。

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()という関数もある。

関数名に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()を使うとよい。

関連カテゴリー

関連記事