Pythonでプログラムを開発していると、既存のリストや辞書を複製して新しい変数を作成したい場面が多々あります。
しかし、単にイコール記号を使って代入するだけでは、期待通りに動作しないケースが少なくありません。
Pythonにおける「コピー」には、大きく分けて浅いコピー(shallow copy)と深いコピー(deep copy)の2種類が存在します。
これらの違いを正しく理解していないと、意図しないデータの書き換えによる深刻なバグを引き起こす可能性があります。
本記事では、Python 3.12以降の最新の言語仕様も踏まえつつ、copyモジュールを使用した複製手法の違いを詳しく解説します。
代入演算子とコピーの決定的な違い
Pythonにおいて、変数への代入はオブジェクトの複製ではなく、「オブジェクトへの参照」を作成する操作です。
まずは、代入演算子(=)を使用した際の挙動を確認してみましょう。
# リストの作成
list_a = [1, 2, 3]
# 代入による参照の共有
list_b = list_a
# list_bの内容を変更
list_b.append(4)
print(f"list_a: {list_a}")
print(f"list_b: {list_b}")
print(f"Same object? {list_a is list_b}")
list_a: [1, 2, 3, 4]
list_b: [1, 2, 3, 4]
Same object? True
このコードを実行すると、list_bに要素を追加したはずなのに、元のlist_aの内容まで書き換わっていることがわかります。
これは、両方の変数がメモリ上の全く同じオブジェクトを指し示しているためです。
このように、単なる代入では新しいオブジェクトは生成されません。
予期せぬ副作用を避けるためには、参照ではなく実体を複製する手法が必要になります。
浅いコピー(copy.copy)の仕組みと特徴
浅いコピーとは、新しい複合オブジェクト(リストや辞書など)を作成し、そこに元のオブジェクトにある「参照」を挿入する手法です。
標準ライブラリのcopyモジュールに含まれるcopy()関数を使用します。
浅いコピーの基本的な動作
まずは、入れ子になっていない単純なリストで挙動を確認してみましょう。
import copy
list_origin = [1, 2, 3]
# 浅いコピーを実行
list_shallow = copy.copy(list_origin)
list_shallow.append(4)
print(f"list_origin: {list_origin}")
print(f"list_shallow: {list_shallow}")
list_origin: [1, 2, 3]
list_shallow: [1, 2, 3, 4]
この場合、list_originとlist_shallowは別々のオブジェクトとして存在するため、一方の変更が他方に影響することはありません。
しかし、リストの中にさらにリストが含まれているような「多重構造」のデータを扱う場合には注意が必要です。
入れ子構造における落とし穴
浅いコピーは、最上位のリストは新しく作成しますが、その中に含まれる要素が可変オブジェクトである場合、その要素自体は参照のままコピーされます。
import copy
# 入れ子構造のリスト
nested_list = [[1, 2], [3, 4]]
shallow_copied = copy.copy(nested_list)
# 内部のリストに変更を加える
shallow_copied[0][0] = 99
print(f"original: {nested_list}")
print(f"shallow: {shallow_copied}")
original: [[99, 2], [3, 4]]
shallow: [[99, 2], [3, 4]]
実行結果からわかる通り、コピー先の内部リストを変更すると、コピー元の内部リストも書き換わってしまいました。
これが浅いコピーにおける最大の注意点であり、多くのプログラマーが直面する「落とし穴」です。
深いコピー(copy.deepcopy)の仕組みと利点
深いコピー(deepcopy)は、再帰的にすべての要素を複製する手法です。
新しい複合オブジェクトを作成した上で、元のオブジェクトに含まれる要素の「コピー」を、さらにその中に挿入していきます。
すべての階層を独立させる
先ほどの入れ子構造の例を、copy.deepcopy()を使って書き換えてみましょう。
import copy
nested_list = [[1, 2], [3, 4]]
# 深いコピーを実行
deep_copied = copy.deepcopy(nested_list)
# 内部のリストに変更を加える
deep_copied[0][0] = 99
print(f"original: {nested_list}")
print(f"deep: {deep_copied}")
original: [[1, 2], [3, 4]]
deep: [[99, 2], [3, 4]]
深いコピーを使用すると、内部のリストも含めて完全に独立した新しいオブジェクトが生成されます。
そのため、どれだけ複雑なデータ構造であっても、コピー元のデータを破壊することなく安全に操作を行うことが可能です。
深いコピーが適しているケース
深いコピーは、主に以下のような複雑なデータを扱うアプリケーションで利用されます。
- 複雑な設定情報を持つディクショナリのテンプレート作成
- ゲームの状態保存(セーブデータ)などのスナップショット作成
- 再帰的な木構造やグラフ構造を持つデータのバックアップ
copyとdeepcopyの比較表
それぞれの違いを明確にするため、主要な特性を表にまとめました。
| 項目 | 浅いコピー (copy) | 深いコピー (deepcopy) |
|---|---|---|
| 処理速度 | 高速 | 低速(再帰処理のため) |
| メモリ消費量 | 少ない | 多い |
| 内部オブジェクト | 参照を共有する | 実体ごと複製する |
| 主な用途 | 1次元のリストや辞書 | 多次元リスト、複雑なクラスインスタンス |
パフォーマンスに関する注意点
機能面だけで見れば、常にdeepcopyを使えば安全であるように思えます。
しかし、実務上の開発において安易なdeepcopyの多用は推奨されません。
深いコピーはオブジェクト全体を走査して再帰的にコピーを生成するため、計算コストとメモリ消費量が大幅に増大します。
数万件の要素を持つ巨大なリストや、複雑な循環参照を含むオブジェクトに対してdeepcopyを実行すると、プログラムのパフォーマンスが著しく低下する恐れがあります。
データ構造がフラットである(入れ子になっていない)ことがわかっている場合は、copy.copy()やリストのスライス操作(list[:])を選択するのが効率的です。
特殊な挙動:不変オブジェクトの場合
Pythonのコピーにおいて、数値や文字列、タプルなどの不変(イミュータブル)なオブジェクトは特殊な挙動を示します。
不変オブジェクトに対してコピーを試みても、Pythonの最適化機構により、実際には新しいオブジェクトが作られず、同じ参照が返されることがあります。
import copy
tuple_a = (1, 2, 3)
tuple_b = copy.deepcopy(tuple_a)
print(f"Same instance? {tuple_a is tuple_b}")
Same instance? True
これは、内容を変更できないオブジェクトであれば、わざわざメモリを消費して複製を作る必要がないためです。
ただし、タプルの中に可変オブジェクト(リストなど)が含まれている場合は、deepcopyによってしっかりと複製が行われます。
自作クラスでのコピー動作のカスタマイズ
自分で定義したクラスにおいて、コピー時の挙動を制御したい場合があります。
そのようなときは、特殊メソッドである__copy__()や__deepcopy__()を実装します。
import copy
class MyData:
def __init__(self, value):
self.value = value
def __deepcopy__(self, memo):
# 独自のコピーロジックを記述
new_instance = MyData(copy.deepcopy(self.value, memo))
return new_instance
このように実装することで、大規模なシステムにおいて特定のフィールドだけをコピーの対象外にする、といった柔軟な設計が可能になります。
ライブラリ開発やフレームワークの構築を行う際には、これらの内部的な仕組みが非常に重要となります。
まとめ
Pythonにおけるオブジェクトの複製は、プログラムの堅牢性に直結する重要な概念です。
単純な代入は単なる参照の共有であり、一方の変更が他方に波及することを常に意識しなければなりません。
1次元のデータ構造を素早く複製したい場合は、処理の軽いcopy.copy()を利用するのが最適です。
一方で、入れ子になったリストや複雑な階層構造を持つデータを完全に独立させたい場合は、copy.deepcopy()が唯一の安全な選択肢となります。
データの構造とアプリケーションの要件に合わせて、適切な複製手法を使い分けることが、バグの少ないクリーンなコードを書くための第一歩です。
今回の内容を参考に、自身のプロジェクトで扱っているデータが「参照」なのか「実体」なのかを、今一度確認してみてください。
