Python の copy モジュールでオブジェクトをコピーする方法 - 浅いコピーと深いコピーの違い
Python の copy と deepcopy 関数を使って、オブジェクトを独立したコピーとして複製する方法について解説します。ミュータブル/イミュータブルの違いや、浅いコピー・深いコピーの使い分けについても解説していますPython の copy とは
Python の copy モジュールには copy()(浅いコピー)と deepcopy()(深いコピー)の 2 つの関数があり、どちらもオブジェクトを複製するときに使います。copy モジュールを import することで利用できます。
今回は Python でオブジェクトをコピーする方法と、copy と deepcopy の違いを解説します。
オブジェクトの構造(入れ子の有無)によってはうまくコピーできないこともあるので、こちらについても解説しています。
copy 関数の基本的な記述方法は下記の通りです。
import copy
test1 = copy.copy(test) # 浅いコピー
test2 = copy.deepcopy(test) # 深いコピー
list 型など一部の組み込み型では、以下のように 型が持つ組み込みメソッド でも浅いコピーが可能です。これらは copy.copy() と等価な結果になります。
test1 = test.copy() # list.copy()
test1 = list(test) # 型のコンストラクタ
test1 = test[:] # スライス
注意:list には deepcopy メソッドはありません。深いコピーを行うには copy.deepcopy() を使う必要があります。
Python で copy を使う際には、ミュータブル/イミュータブルといったオブジェクトの性質と、「代入」がどのように動くかを理解しておく必要があります。
オブジェクトのミュータブルとイミュータブル
Python のオブジェクトは、生成後にその場で変更できる ミュータブル なオブジェクトと、変更できない イミュータブル なオブジェクトに分けられます。
| オブジェクトの種類 | データの種類 |
|---|---|
| ミュータブル | list、dict、set、bytearray など |
| イミュータブル | int、float、str、tuple、bytes、frozenset など |
ここで押さえておきたいのは、Python では 変数への代入は「同じオブジェクトを新しい名前で束縛する」だけ ということです。ミュータブル・イミュータブルにかかわらず、代入直後は id() が一致します。違いは「その後に値を変えたとき」の挙動です。
ミュータブルの場合
test1 = ['a', 'b', 'c']
test2 = test1 # 同じ list を test1, test2 の 2 名前で束縛
test1.append('d') # その場で変更(in-place)
print(test1) # ['a', 'b', 'c', 'd']
print(test2) # ['a', 'b', 'c', 'd'] ← test2 も影響を受ける
print(id(test1)) # 例: 22668883604800
print(id(test2)) # 例: 22668883604800 同一
list はミュータブルなので、append() のように「その場で」中身を書き換えられます。両方の名前が同じオブジェクトを指しているため、片方から見た変更がもう一方にも見えます。
イミュータブルの場合
test1 = 10
test2 = test1
print(id(test1)) # 例: 9801536
print(id(test2)) # 例: 9801536 同一
test1 = 0 # 「10 を 0 に変更」ではなく「test1 という名前を別のオブジェクト 0 に付け替える」
print(test1) # 0
print(test2) # 10 ← 影響を受けない
print(id(test1)) # 例: 9801216
print(id(test2)) # 例: 9801536 変わらない
int はイミュータブルなので、test1 の値を「変える」ことはできません。代わりに test1 という名前を別のオブジェクト 0 に付け替えているだけです。test2 は元の 10 を指したままなので、影響を受けません。
代入とコピーは別物
「Python は参照渡し?値渡し?」という質問を耳にすることがありますが、Python の代入・引数渡しは「call by object reference(オブジェクト参照渡し)」あるいは「call by sharing」と呼ばれる方式で、C++ の参照渡しでも値渡しでもありません。
ポイントはただひとつ:「代入はオブジェクトをコピーしない」。オブジェクトを独立したコピーとして複製したいなら copy.copy() や copy.deepcopy() を使う必要があります。
copy の使い方(浅いコピー)
実際に浅いコピーで別オブジェクトを作る例です。
import copy
test = ['apple', 'orange']
test1 = copy.copy(test) # あるいは test.copy() / test[:] / list(test)
test.append('peach')
print(test) # ['apple', 'orange', 'peach']
print(id(test)) # 例: 22789604275520
print(test1) # ['apple', 'orange']
print(id(test1)) # 例: 22789603397056 異なる
copy.copy() でコピーしたオブジェクトはオブジェクト ID が異なり、それぞれ独立したオブジェクトになっています。
元の test に append() で値を追加しても、test1 は影響を受けません。
copy と deepcopy の違い
copy.copy() は「一番外側のコンテナだけ」を新しく作ります。中に入っている要素は元のオブジェクトと共有されるため、入れ子構造(list の中に list を入れる、dict の中に list を入れる等)の場合は完全にコピーできません。copy.deepcopy() は再帰的にコピーするため、中身まで完全に独立させられます。循環参照が含まれていても内部で検出して処理してくれます。
2 次元リストで違いを見てみましょう。
copy.copy の場合
import copy
test = [['a', 'b'], ['c', 'd']]
test1 = copy.copy(test)
test[1].append('e') # 内側のリストを書き換え
print(test) # [['a', 'b'], ['c', 'd', 'e']]
print(id(test)) # 例: 23037493484800
print(test1) # [['a', 'b'], ['c', 'd', 'e']] ← 影響を受けている
print(id(test1)) # 例: 23037493484864 外側は別
外側のリストは別のオブジェクトになっていますが、内側のリストは共有されたままなので、test[1].append('e') が test1 にも反映されています。
copy.deepcopy の場合
import copy
test = [['a', 'b'], ['c', 'd']]
test1 = copy.deepcopy(test)
test[1].append('e')
print(test) # [['a', 'b'], ['c', 'd', 'e']]
print(id(test)) # 例: 22764581496320
print(test1) # [['a', 'b'], ['c', 'd']] ← 影響なし
print(id(test1)) # 例: 22764581495680 外側も内側も別
deepcopy なら内側のリストまで独立しているので、元の変更は反映されません。
その他の話題
独自クラスのコピー挙動をカスタマイズする
クラスに __copy__ / __deepcopy__ メソッドを実装すると、copy.copy() / copy.deepcopy() の挙動を自分でコントロールできます。DB コネクションのような「コピーすべきでない属性」を持つクラスで有用です。
dataclass のコピー
dataclasses.replace(obj, field=value) は「一部のフィールドだけ差し替えた新しいインスタンス」を返す便利関数です。関数型スタイルでイミュータブルなデータ更新をしたいときに使えます。
deepcopy のコスト
deepcopy は再帰的にオブジェクトを辿るためコストが高くなりがちです。大きなデータ構造を頻繁にコピーする場面では、pickle 経由のコピー、専用の変換関数、あるいは不変データ構造の採用など、別のアプローチも検討しましょう。
まとめ
オブジェクトをコピーしたいときは、入れ子構造の有無 によって copy と deepcopy を使い分けましょう。
- 入れ子が無い(要素がすべてイミュータブル、あるいは共有されても構わない):
copy.copy()(またはlist.copy()などの型メソッド)で十分。 - 入れ子構造があり、内側まで独立させたい:
copy.deepcopy()。
また、Python の「代入」と「コピー」を混同しないことが最重要ポイントです。代入では新しいオブジェクトは作られません。ミュータブルなオブジェクトを扱うときはとくに、意図しない共有によるバグを避けるため copy モジュールを積極的に活用してください。













