note.nkmk.me

NumPy配列ndarrayがビューかコピーか、メモリを共有しているか判定

Date: 2019-09-15 / tags: Python, NumPy

NumPy配列numpy.ndarrayがビューかコピーかを判定するにはbase属性、メモリを共有しているかを判定するにはnp.shares_memory()関数を使う。

ここでは以下の内容について説明する。

  • numpy.ndarrayのビューとコピー(メモリの共有)
  • ビューかコピーか判定: base属性
  • メモリを共有しているか判定: np.shares_memory()
    • 基本的な使い方
    • np.may_share_memory()との違い

pandas.DataFrameにおけるビューとコピーについては以下の記事を参照。

以下の内容のNumPyのバージョンは1.16.4。バージョンが異なると挙動が異なる可能性があるので注意

スポンサーリンク

numpy.ndarrayのビューとコピー(メモリの共有)

NumPy配列ndarrayにはビュー(view)とコピー(copy)がある。

ある配列オブジェクトから別の配列オブジェクトを生成する場合、元のオブジェクトとメモリを共有する(元のオブジェクトのメモリの一部または全部を参照する)オブジェクトをビュー、元のオブジェクトと別にメモリを新たに確保するオブジェクトをコピーという。

例えばスライスはビューを生成する。

import numpy as np

a_2d = np.arange(12).reshape(3, 4)
print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

a_slice = a_2d[:2, :2]
print(a_slice)
# [[0 1]
#  [4 5]]

同じメモリを参照しているので、一方のオブジェクトの要素の値を変更すると他方の値も変更される。

a_slice[0, 0] = 100
print(a_slice)
# [[100   1]
#  [  4   5]]

print(a_2d)
# [[100   1   2   3]
#  [  4   5   6   7]
#  [  8   9  10  11]]

a_2d[0, 0] = 0
print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

print(a_slice)
# [[0 1]
#  [4 5]]

ファンシーインデックスはコピーを生成する。

a_fancy_index = a_2d[[0, 1]]
print(a_fancy_index)
# [[0 1 2 3]
#  [4 5 6 7]]

一方のオブジェクトの要素の値を変更しても他方の値は変更されない。

a_fancy_index[0, 0] = 100
print(a_fancy_index)
# [[100   1   2   3]
#  [  4   5   6   7]]

print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

ビューからコピーを生成するにはcopy()を使う。

a_slice_copy = a_2d[:2, :2].copy()
print(a_slice_copy)
# [[0 1]
#  [4 5]]

一方のオブジェクトの要素の値を変更しても他方の値は変更されない。

a_slice_copy[0, 0] = 100
print(a_slice_copy)
# [[100   1]
#  [  4   5]]

print(a_2d)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

なお、view()というメソッドもあるが、これはあくまでも呼び出し元のビューを生成するもの。例えば、ファンシーインデックスで生成したオブジェクトからview()を実行してもコピーのビューが生成されるだけで、元のオブジェクトのビューが生成されるわけではない。

ビューは巨大なnumpy.ndarrayの一部を処理する際に便利な仕組みだが、別々に処理したい場合は知らないうちに値が変わってしまうことがあるため注意が必要。また、ビューだと思っていたらコピーで値が変わってなかったということも起こり得る。

ビューかコピーか判定: base属性

numpy.ndarrayがビューかコピーか(厳密にはビューかそうでないか)を判定するにはbase属性を使う。

numpy.ndarrayがビューである場合、base属性はオリジナルのnumpy.ndarrayを示す。形状を変更する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]

a_1 = a_0.reshape(2, 3)
print(a_1)
# [[0 1 2]
#  [3 4 5]]

print(a_0.base)
# [0 1 2 3 4 5 6 7 8 9]

print(a_1.base)
# [0 1 2 3 4 5 6 7 8 9]

コピーや元のnumpy.ndarray(コピーでもビューでもなく新たに生成したnumpy.ndarray)はNone

a_copy = a.copy()
print(a_copy)
# [0 1 2 3 4 5 6 7 8 9]

print(a_copy.base)
# None

print(a.base)
# None

is演算子を使ってNoneと比較するとビューかそうでないかを判定できる。

print(a_0.base is None)
# False

print(a_copy.base is None)
# True

print(a.base is None)
# True

元のnumpy.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つのnumpy.ndarrayがメモリを共有しているかはnp.shares_memory()関数で判定できる。

基本的な使い方

np.shares_memory()に判定したい2つのnumpy.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

ビュー同士でもOK。

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()というnp.shares_memory()と似た関数もある。

関数名にmayが含まれていることからも分かるように、np.may_share_memory()np.shares_memory()に比べて厳密ではない。

np.may_share_memory()はメモリアドレスの範囲がオーバーラップしているかどうかを判定するのみで、同じメモリを参照している要素があるかどうかは考慮しない。

例えば以下のような場合。2つのスライスは同じnumpy.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つのスライスが元のnumpy.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)
# 839 ns ± 53.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

%%timeit
np.may_share_memory(a_0, a_1)
# 275 ns ± 5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

np.may_share_memory()は各要素がメモリを共有していない場合に誤ってTrueを返す可能性があるが、メモリを共有しているのに誤ってFalseを返すことはない。

スポンサーリンク
シェア
このエントリーをはてなブックマークに追加

関連カテゴリー

関連記事