NumPy配列ndarrayのビューとコピー(メモリの共有) | note.nkmk.me (original) (raw)
NumPy配列numpy.ndarray
におけるビュー(view)とコピー(copy)について説明する。
ndarray
のコピーを生成するにはcopy()
メソッド、ndarray
が他のndarray
のビューかコピーかを判定するにはbase
属性、2つのndarray
がメモリを共有しているかを判定するにはnp.shares_memory()
やnp.may_share_memory()
関数を使う。
目次
- NumPy配列ndarrayのビューとコピー
- NumPy配列ndarrayのコピーを生成: copy()
- ビューかコピーか判定: base属性
- メモリを共有しているか判定: np.shares_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()
のほうが便利。
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()
という関数もある。
- 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()
を使うとよい。