Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

copyとdeepcopy

リストや配列の代入(=)は中身を複製せず,同じオブジェクトに別名を付けるだけ. そのため,一方を書き換えると他方も変更される. 各世代の状態を記録するシミュレーション(ライト-フィッシャー モデル (Wright-Fisher model),ライフゲームなど)ではコピーの使い分けが重要になる.

変数とオブジェクト:代入はコピーではない

Python では値はすべてオブジェクト (object)としてメモリ上に置かれ,変数 (variable)はそのオブジェクトに付けられたラベルにすぎない. 代入 b = a は,a が指すオブジェクトを複製するのではなく,同じオブジェクトに b という2枚目のラベルを付ける操作にあたる.

a = [3, 1, 2]
b = a

# is はオブジェクトの同一性を判定する(同じオブジェクトなら True)
print("a is b:", a is b)
a is b: True

a is bTrue であることから,ab は同じリストオブジェクトを参照 (reference)しているとわかる.

共有による思わぬ書き換え(エイリアス)

同じオブジェクトを指す複数の変数をエイリアス (alias)という. エイリアスの一方を通じて中身を書き換えると,もう一方から見た値も変わる.

a = [3, 1, 2]
b = a
b[2] = 100  # b 経由で書き換える

print("b:", b)
print("a:", a)  # a も変わってしまう
b: [3, 1, 100]
a: [3, 1, 100]

これはリストがミュータブル (mutable)(変更可能)なオブジェクトだから起こる. 数値や文字列はイミュータブル (immutable)(変更不可能)で,「書き換え」のかわりに新しいオブジェクトへのラベルの付け替えが起こるため,この問題は生じない.

x = 5
y = x
y = y + 1  # 新しい整数オブジェクト 6 に y を付け替える

print("y:", y)
print("x:", x)  # x は 5 のまま
y: 6
x: 5

ループでの予期せぬ書き換え

この落とし穴は,ループで状態を記録するときに顕在化しやすい. 次のコードは,1つのリスト state を書き換えながら記録用リスト history に追加していく.

state = [0, 0, 0]
history = []
for i in range(3):
    for j in range(3):
        state[j] = i
    history.append(state)  # 同じオブジェクトを追加している

print(history)
[[2, 2, 2], [2, 2, 2], [2, 2, 2]]

各世代で異なる値を記録したはずが,history の中身はすべて同じになってしまう. history に追加されたのは毎回同じ state オブジェクトへの参照であり,最後の書き換え結果が全要素に反映されるため.

浅いコピー shallow copy

この問題は,記録するたびに独立した複製を追加すれば解決する. リストの複製を返すのが浅いコピー (shallow copy)で,list.copy() メソッド,スライス state[:]copy モジュールの copy.copy() のいずれでも作れる.

state = [0, 0, 0]
history = []
for i in range(3):
    for j in range(3):
        state[j] = i
    history.append(state.copy())  # 独立した複製を追加する

print(history)
[[0, 0, 0], [1, 1, 1], [2, 2, 2]]

各世代の状態が正しく記録される.

a = [3, 1, 2]
b = a.copy()  # b = a[:] や copy.copy(a) でも同じ
b[2] = 100

print("b:", b)
print("a:", a)  # a は変わらない
b: [3, 1, 100]
a: [3, 1, 2]

深いコピー deep copy

浅いコピーが複製するのは最上位のオブジェクトだけで,中に入っている要素は元と共有されたまま. 要素自体がリストであるネストした構造(リストのリスト,2次元配列など)では,これが問題になる.

a = [[1, 2], [3, 4]]
b = a.copy()  # 浅いコピー
b[0][0] = 100  # 内側のリストを書き換える

print("b:", b)
print("a:", a)  # 内側のリストは共有されているので a も変わる
b: [[100, 2], [3, 4]]
a: [[100, 2], [3, 4]]

b 自体は別オブジェクトだが,b[0]a[0] は同じ内側リストを指しているため,要素の書き換えが波及する. 要素まで含めて完全に独立した複製がほしいときは,copy.deepcopy() を使う.これは深いコピー (deep copy)とよばれる.

import copy

a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)  # 深いコピー
b[0][0] = 100

print("b:", b)
print("a:", a)  # a は変わらない
b: [[100, 2], [3, 4]]
a: [[1, 2], [3, 4]]

浅いコピーと深いコピーの違い

代入・浅いコピー・深いコピーの違いは,「最上位のオブジェクト」と「ネストした要素」のそれぞれが独立か共有かで整理できる.

操作書き方最上位ネストした要素
代入b = a共有共有
浅いコピーa.copy()a[:]copy.copy(a)独立共有
深いコピーcopy.deepcopy(a)独立独立

ネストのない平らなリスト(数値だけのリストなど)であれば,浅いコピーで十分である.

NumPy配列:ビューとコピー

NumPy配列 (ndarray)のコピーには,リストにはない注意点がある. 配列のスライス (slicing)は新しい配列を作らず,元の配列とメモリを共有するビュー (view)を返す.リストのスライスがコピーを返すのとは異なる.

import numpy as np

a = np.array([0, 1, 2, 3, 4])
b = a[1:4]  # スライスはビュー(コピーではない)
b[0] = 100

print("b:", b)
print("a:", a)  # 元の配列にも波及する
b: [100   2   3]
a: [  0 100   2   3   4]

独立した複製がほしいときは,np.copy() または .copy() メソッドを使う.

a = np.array([0, 1, 2, 3, 4])
b = np.copy(a)  # a.copy() でも同じ
b[0] = 100

print("b:", b)
print("a:", a)  # a は変わらない
b: [100   1   2   3   4]
a: [0 1 2 3 4]

セルオートマトンや拡散方程式では,配列をスライスで近傍を取り出しながら次の状態を計算する. 元の配列を上書きしてよいのか,複製してから書き換えるべきかを,ビューとコピーの違いをふまえて判断する必要がある.

まとめ

やりたいこと使うもの
同じオブジェクトに別名を付ける代入 b = a
平らなリスト・配列を独立に複製するa.copy()np.copy(a)
ネストした構造を要素まで独立に複製するcopy.deepcopy(a)

浅いコピーと深いコピーの詳細は公式ドキュメントの copy モジュールも参照してほしい.