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()はいずれも浅いコピーで、外側のリストは別物になりますが、ネストした内側のリストは元と共有されます。
- スライスによるコピー
copied_list = self.my_list[:] list()コンストラクタによるコピーcopied_list = list(self.my_list)-
deepcopyによるコピー(ネストされたリストの場合)
ネストされたリストが含まれる場合、copy()では浅いコピーしか行われません。deepcopyを使用して深いコピーを作成できます。import copy copied_list = copy.deepcopy(self.my_list)
まとめ
- インスタンス変数のNumPy配列をそのまま返した場合の、呼び出し側の変更のクラス内部への反映
self.points.copy()で返すことによる、元データから独立した配列の受け渡し- NumPy配列におけるスライスと
list()の、コピーにならない点 - ネストした通常のリストに対する、
copy.deepcopyによる深いコピーの作成
