【Python入門】浅いコピーと深いコピーの違いをわかりやすく解説【初心者向け完全ガイド】
目次
はじめに
Pythonでプログラミングをしていると、リストや辞書などのデータをコピーする場面に必ず遭遇します。しかし、単純に代入するだけでは思わぬバグを引き起こすことがあります。
「リストをコピーしたはずなのに、元のデータまで変わってしまった」という経験はありませんか?これは、Pythonのコピーの仕組みを正しく理解していないために起こる典型的な問題です。
本記事では、Python初心者が必ず押さえておくべき「浅いコピー(shallow copy)」と「深いコピー(deep copy)」の違いについて、基礎から実践的な使い分けまで徹底的に解説します。
なぜコピーの理解が重要なのか
Pythonでは、変数は実際のデータそのものではなく、データへの「参照」を保持しています。これはメモリ効率を高めるための仕組みですが、正しく理解しないと予期せぬ動作を引き起こします。
特にリスト、辞書、セットなどのミュータブル(変更可能)なオブジェクトを扱う際には、コピーの仕組みを理解することが不可欠です。データの独立性を確保し、バグのない堅牢なコードを書くために、この知識は必須となります。
参照とは何か – コピーを理解する前提知識
浅いコピーと深いコピーを理解する前に、まずPythonの「参照」という概念を理解する必要があります。
Pythonでは、変数に値を代入すると、その変数はデータそのものを保持するのではなく、データが格納されているメモリ上の場所(アドレス)を参照します。つまり、変数は「データの場所を示す矢印」のようなものです。
複数の変数が同じオブジェクトを参照している場合、どれか一つの変数を通じてオブジェクトを変更すると、他の変数から見ても変更が反映されます。これは、すべての変数が同じメモリ上の場所を指しているためです。
この仕組みを理解していないと、「変数Aを変更したのに、変数Bまで変わってしまった」という混乱が生じます。
浅いコピー(Shallow Copy)とは
浅いコピーは、オブジェクトの最上位レベルのみを新しく複製し、内部に含まれるオブジェクトは元のオブジェクトと共有する方法です。
浅いコピーの特徴
浅いコピーでは、新しいコンテナオブジェクト(リストや辞書など)が作成されますが、その中身の要素は元のオブジェクトと同じものを参照し続けます。つまり、「箱は新しく作られるが、中身は共有されている」状態です。
リストの例で考えると、リスト自体は別物として複製されますが、リストの中に別のリストやオブジェクトが含まれている場合、その内部のオブジェクトは元のリストと共有されます。
浅いコピーの実行方法
Pythonで浅いコピーを作成する方法はいくつかあります:
- copyモジュールのcopy()関数を使用
- リストのスライス記法[:]を使用
- list()、dict()などのコンストラクタを使用
- リストのcopy()メソッドを使用
これらの方法は基本的に同じ結果をもたらし、いずれも浅いコピーを作成します。
import copy
# 元のリスト
original = [1, 2, [3, 4]]
# 浅いコピーの作成
shallow = copy.copy(original)
# または shallow = original[:]
# または shallow = original.copy()
# トップレベルの要素を変更
shallow[0] = 100 # originalには影響しない
# ネストされたリストを変更
shallow[2][0] = 999 # originalにも影響する!
浅いコピーで注意すべき点
浅いコピーの最大の注意点は、ネストされた構造(リストの中のリストなど)を持つオブジェクトをコピーする場合です。
コピーされたオブジェクトの内部にある別のオブジェクトを変更すると、元のオブジェクトにも影響が及びます。これは、内部のオブジェクトが共有されているためです。
この動作は直感に反するため、多くの初心者がここで混乱します。「コピーしたのに、なぜ元のデータが変わるのか?」という疑問を持つのは自然なことですが、これが浅いコピーの本質的な挙動なのです。
深いコピー(Deep Copy)とは
深いコピーは、オブジェクトとその内部に含まれるすべてのオブジェクトを再帰的に完全に複製する方法です。
深いコピーの特徴
深いコピーでは、元のオブジェクトとは完全に独立した新しいオブジェクトが作成されます。コンテナオブジェクトだけでなく、その中に含まれるすべての要素も新しく複製されます。
「箱も中身も、すべて新しく作り直される」というイメージです。そのため、コピーされたオブジェクトに対してどのような変更を加えても、元のオブジェクトには一切影響しません。
深いコピーの実行方法
深いコピーを作成するには、copyモジュールのdeepcopy()関数を使用します。これは深いコピーを作成する唯一の標準的な方法です。
import copy
# 複雑なネスト構造を持つリスト
original = [1, 2, [3, 4, [5, 6]]]
# 深いコピーの作成
deep = copy.deepcopy(original)
# どの階層を変更しても元のデータに影響しない
deep[2][0] = 999
deep[2][2][0] = 777
# originalは変更されていない
print(original) # [1, 2, [3, 4, [5, 6]]]
深いコピーのパフォーマンス考慮
深いコピーは非常に便利ですが、オブジェクトの構造全体を再帰的にコピーするため、浅いコピーと比較して処理時間とメモリ使用量が大きくなります。
特に、巨大なデータ構造や深くネストされた複雑なオブジェクトをコピーする場合は、パフォーマンスへの影響を考慮する必要があります。不必要に深いコピーを使用すると、プログラムの実行速度が低下する可能性があります。
また、循環参照(オブジェクトが自分自身を参照している状態)を含むオブジェクトをコピーする場合、deepcopy()は適切に処理しますが、自作のコピー処理では無限ループに陥る危険があります。
浅いコピーと深いコピーの具体的な違い
両者の違いを整理すると以下のようになります:
複製の範囲
浅いコピー: 最上位レベルのコンテナのみを複製します。内部の要素は元のオブジェクトと共有されたままです。一階層分だけ独立性が確保されます。
深いコピー: オブジェクト全体を再帰的に複製します。ネストの深さに関係なく、すべての階層が独立した新しいオブジェクトとして作成されます。
独立性
浅いコピー: トップレベルの要素を変更した場合のみ、元のオブジェクトに影響しません。しかし、ネストされた内部のオブジェクトを変更すると、元のオブジェクトにも影響が及びます。
深いコピー: どの階層のデータを変更しても、元のオブジェクトには一切影響しません。完全な独立性が保証されます。
パフォーマンス
浅いコピー: 最上位レベルのみをコピーするため、高速で軽量です。メモリ効率も良好です。
深いコピー: すべての階層を再帰的にコピーするため、処理時間がかかり、メモリ使用量も増加します。データ構造が複雑になるほど、そのコストは顕著になります。
使用場面
浅いコピー: 単純なリストや、ネスト構造を変更する予定がない場合に適しています。多くの一般的な用途では十分です。
深いコピー: 複雑なネスト構造を持ち、完全な独立性が必要な場合に使用します。設定オブジェクトのバックアップや、データの複数バージョンを保持する場合などに有効です。
実践的な使い分けのガイドライン
浅いコピーを使うべき場合
数値や文字列など、イミュータブル(変更不可能)な要素のみで構成されたリストや辞書をコピーする場合は、浅いコピーで十分です。これらの要素は変更できないため、共有されていても問題になりません。
また、ネスト構造があっても内部のオブジェクトを変更する予定がない場合や、意図的に内部データを共有したい場合も浅いコピーが適切です。
パフォーマンスが重要で、データ量が大きい場合も、可能であれば浅いコピーを選択すべきです。
深いコピーを使うべき場合
リストの中にリストがあるような複雑なネスト構造を持ち、内部のデータも独立して変更する必要がある場合は、深いコピーが必須です。
オブジェクトの完全なスナップショットを作成したい場合、例えばゲームのセーブデータや設定の保存、元に戻す(Undo)機能の実装などでは、深いコピーを使用します。
また、マルチスレッド環境で複数のスレッドが同じデータを変更する可能性がある場合、データの独立性を確保するために深いコピーが有効です。
判断基準
コピーの方法を選択する際の基本的な判断基準は以下の通りです:
- データ構造の複雑さ: ネスト構造があるか?
- 変更の範囲: 内部のオブジェクトも変更するか?
- 独立性の必要性: 完全に独立したコピーが必要か?
- パフォーマンス要件: 処理速度とメモリ使用量の制約はあるか?
これらの要素を総合的に判断し、適切なコピー方法を選択することが重要です。
よくある間違いとその対策
単純な代入をコピーと勘違いする
最も一般的な間違いは、new_list = old_listのような単純な代入をコピーだと思い込むことです。これはコピーではなく、単に同じオブジェクトへの別の参照を作成しているだけです。
この場合、new_listとold_listは完全に同じオブジェクトを指しているため、どちらを変更しても両方に影響します。これは浅いコピーですらなく、単なる参照の複製です。
浅いコピーで十分な場面で深いコピーを使う
逆に、不必要に深いコピーを使用することも問題です。単純なリストをコピーするだけなのに、毎回deepcopy()を使用すると、パフォーマンスの無駄が生じます。
深いコピーは強力ですが、常に使うべきものではありません。必要性を正しく判断し、適切な方法を選択することが重要です。
ネスト構造の深さを考慮しない
リストに別のリストが含まれていることに気づかず、浅いコピーを使用して予期せぬバグを生むケースも多く見られます。
データ構造を設計する際、またはコピーを実装する際は、常にネストの深さと内容を意識することが重要です。
イミュータブルなオブジェクトとコピー
Pythonの文字列、数値、タプルなどのイミュータブル(変更不可能)なオブジェクトの場合、コピーの概念は異なる意味を持ちます。
これらのオブジェクトは変更できないため、浅いコピーも深いコピーも実質的に同じ結果となります。Pythonは内部的に最適化を行い、イミュータブルなオブジェクトは必要に応じて共有されます。
そのため、イミュータブルな要素のみで構成されたリストをコピーする場合、浅いコピーで問題なく、深いコピーの必要性は通常ありません。
辞書とセットのコピー
リストと同様に、辞書やセットなどの他のコレクション型でも、浅いコピーと深いコピーの概念は適用されます。
辞書の場合、キーと値のペアが複製されますが、浅いコピーでは値が他のオブジェクトへの参照である場合、その参照は共有されます。辞書の値がリストや別の辞書である場合は特に注意が必要です。
セットについても同様で、セットの要素が変更可能なオブジェクトへの参照を含む場合(ただしセットの要素はハッシュ可能である必要があるため、実際には限定的)、浅いコピーの挙動を理解しておく必要があります。
まとめ
Pythonの浅いコピーと深いコピーの違いを理解することは、バグのない堅牢なコードを書くために不可欠です。
浅いコピーは最上位レベルのみを複製し、高速で軽量ですが、ネストされた構造の内部は元のオブジェクトと共有されます。単純なデータ構造や、内部を変更しない場合に適しています。
深いコピーはオブジェクト全体を再帰的に複製し、完全な独立性を提供しますが、パフォーマンスコストがあります。複雑なネスト構造で完全な独立性が必要な場合に使用します。
重要なのは、データの構造と使用目的に応じて適切な方法を選択することです。常に深いコピーを使えば安全というわけではなく、不必要なコピーはパフォーマンスの低下を招きます。
データ構造を設計する段階で、コピーの必要性とその方法を考慮し、参照の共有が問題になりそうな場面を事前に識別することが、高品質なPythonコードを書く秘訣です。
この知識を活かして、より信頼性の高いPythonプログラムを作成していきましょう。
■「らくらくPython塾」が切り開く「呪文コーディング」とは?
■プロンプトだけでオリジナルアプリを開発・公開してみた!!
■初心者歓迎「AI駆動開発/生成AIエンジニアコース」はじめました!
テックジム東京本校で先行開始。
■テックジム東京本校
格安のプログラミングスクールといえば「テックジム」。
講義動画なし、教科書なし。「進捗管理とコーチング」で効率学習。
対面型でより早くスキル獲得、月額2万円のプログラミングスクールです。
<短期講習>5日で5万円の「Pythonミニキャンプ」開催中。
<オンライン無料>ゼロから始めるPython爆速講座


