NumPy配列をcopy()で返して元データを守る方法

クラスのインスタンス変数にNumPy配列を持たせ、getterでそのまま返すと、呼び出し側で配列を書き換えたときにクラス内部のデータまで変わってしまいます。防ぐには、self.points.copy()のようにコピーを作って返します。
原因は、returnが配列そのものではなく、配列への参照(同じデータを指す名前)を返すことです。この記事では、実際のコードで問題を再現し、copy()で解決する手順と、コピーにならない落とし穴を解説します。

こんな悩みを解決します

  • getterで受け取った配列の変更が、クラス内部の値にまで及ぶ問題
  • 呼び出し元の変更からクラスの状態を守る方法
  • 複数あるコピー方法の使い分け

問題:配列をそのまま返すと元データが書き換わる

次のコードでは、get_points()がself.pointsをそのまま返しています。

import numpy as np

class TestClass:
    def __init__(self):
        self.points = np.array([(1,1), (100,1), (10,100), (1,100)], dtype=np.int32)

    def get_points(self):
        return self.points
#;

test = TestClass()
points1 = test.get_points()
print('before-----')
print(points1)
points1[1][0] = 50
print(f'points1={points1[1]}')
points2 = test.get_points()
print('after-----')
print(points2)
before-----
[[  1   1]
 [100   1]
 [ 10 100]
 [  1 100]]
points1=[50  1]
after-----
[[  1   1]
 [ 50   1]
 [ 10 100]
 [  1 100]]

points1とself.pointsは同じ配列を指しています。そのためpoints1[1][0] = 50と書き換えただけで、get_points()で取り直したpoints2にも50が入っています。出力のafterの部分が、その証拠です。

解決策:copy()でコピーして返す

返す直前にcopy()を呼び、元データとは別の配列を渡します。

import numpy as np

class TestClass:
    def __init__(self):
        self.points = np.array([(1,1), (100,1), (10,100), (1,100)], dtype=np.int32)

    def get_points(self):
        return self.points.copy()
#;

test = TestClass()
points1 = test.get_points()
print('before-----')
print(points1)
points1[1][0] = 50
print(f'points1={points1[1]}')
points2 = test.get_points()
print('after-----')
print(points2)
before-----
[[  1   1]
 [100   1]
 [ 10 100]
 [  1 100]]
points1=[50  1]
after-----
[[  1   1]
 [100   1]
 [ 10 100]
 [  1 100]]

先ほどと同じ書き換えをしても、points2は元の値のままです。呼び出し側は自分用のコピーを受け取るので、クラス内部のself.pointsには影響しません。
コピーした分だけメモリを使います。大きな配列を何度も取得する処理では、この点に注意してください。

NumPy配列のコピー方法

NumPy配列で、元データから独立したコピーになるかどうかを整理します。

方法 コピーになるか 内容
arr.copy() なる 最も基本的な方法
np.copy(arr) なる 関数形式で、結果は同じ
copy.deepcopy(arr) なる 標準ライブラリcopyを使う方法
arr[:] ならない view(元データを共有)
list(arr) ならない 行ごとのviewを並べたリスト

コピーになる方法

arr.copy()、np.copy(arr)、copy.deepcopy(arr)はいずれも、元データから独立した配列を作ります。数値型の配列では、どれも深いコピー相当です。要素がPythonオブジェクトの配列(dtype=object)では、copy()は要素への参照をコピーするだけなので、中身まで切り離したい場合はcopy.deepcopyを使います。

配列ではコピーにならない方法に注意

リストでおなじみの書き方は、NumPy配列ではコピーになりません。

  • arr[:]のスライス(viewを返し、元データとメモリを共有するため、片方の変更が元にも反映)
  • list(arr)(行ごとのviewを並べたリストを返し、中身は元の配列と共有されたまま)

viewとは、元データを別の見方で参照しているだけの配列です。メモリを共有しているかどうかは、np.shares_memory(a, b)で確認できます。arr[:]の結果と元の配列を渡すとTrueになります。
getterでself.points[:]やlist(self.points)を返しても、問題のコードと同じ状態のままです。

通常のPythonリストの場合

ここまではNumPy配列の話です。Python標準のリスト(list)では事情が変わります。copy()、スライス、list()はいずれも浅いコピーで、外側のリストは別物になりますが、ネストした内側のリストは元と共有されます。

  1. スライスによるコピー
    copied_list = self.my_list[:]
  2. list()コンストラクタによるコピー
    copied_list = list(self.my_list)
  3. deepcopyによるコピー(ネストされたリストの場合)
    ネストされたリストが含まれる場合、copy()では浅いコピーしか行われません。deepcopyを使用して深いコピーを作成できます。

    import copy
    
    copied_list = copy.deepcopy(self.my_list)

まとめ

  • インスタンス変数のNumPy配列をそのまま返した場合の、呼び出し側の変更のクラス内部への反映
  • self.points.copy()で返すことによる、元データから独立した配列の受け渡し
  • NumPy配列におけるスライスとlist()の、コピーにならない点
  • ネストした通常のリストに対する、copy.deepcopyによる深いコピーの作成
このエントリーをはてなブックマークに追加
にほんブログ村 IT技術ブログへ

コメント

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です