閉じる

Pythonのシャローコピーvsディープコピー | コピーの違いを解説

Pythonでプログラムを書いていると、変数に代入したはずなのに思わぬところが書き換わっていたり、同じようにコピーしたつもりでも結果が違っていたりすることがあります。

これは、シャローコピー(shallow copy)とディープコピー(deep copy)の違いを正しく理解できていないことが原因である場合が多いです。

本記事では、Pythonのコピーの仕組みを基礎から丁寧に解説し、実務で安全かつ効率的にデータをコピーするための考え方を整理します。

Pythonのシャローコピーとディープコピーとは

コピーの基本

Pythonにおける「コピー」は、単に見た目が同じデータをもう1つ用意することではなく、「オブジェクト」と「参照」の関係を理解することが重要です。

オブジェクトと参照(変数)の関係

Pythonでは、値そのものを変数が直接持つのではなく、オブジェクト(実体)と、それを指し示す参照(アドレスのようなもの)に分かれて管理されています。

代入はコピーではなく「参照の共有」になる点が、最初の重要ポイントです。

このように、次のようなコードはコピーではありません。

Python
a = [1, 2, 3]
b = a  # これは「同じオブジェクトを指す別名」を作っているだけ

このときabは、同じリストオブジェクトを参照しています。

どちらかを変更すると、もう一方から見ても変更されたように見えます。

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

b[0] = 99
print(a)  # aも変わる
print(b)
実行結果
[99, 2, 3]
[99, 2, 3]

「コピー」が指す2つの意味

Pythonで「コピー」と言ったとき、実は2種類あります。

  • シャローコピー(shallow copy)
    外側のコンテナ(リストや辞書など)だけ新しく作り、中身の要素は元のオブジェクトを参照するコピー。
  • ディープコピー(deep copy)
    中に含まれるオブジェクトまで再帰的に新しく作る完全なコピー

この2つの違いと使い分けが、本記事の中心テーマになります。

Pythonでコピーが必要になる典型的なケース

プログラムの中でコピーが必要になるケースは、実はかなり多くあります。

たとえば次のような場面です。

元データを壊したくないとき

関数に渡したデータを、中で書き換えたいが元のデータは保持しておきたい場合があります。

Python
def normalize(scores):
    # ここでscoresを書き換えたいが、呼び出し元のリストは壊したくない…
    pass

このようなとき、関数内部でコピーを作ってから操作するのが安全です。

ログや履歴用にスナップショットを保存するとき

ゲームの状態、機械学習の学習途中のパラメータ、設定値などを「その時点の状態」として保存しておきたいときにもコピーが必要です。

元のデータを更新していっても、過去の状態が変わらないようにするためです。

複数箇所で同じ初期データを使いたいとき

共通のテンプレート的なオブジェクトから、状況に応じて少しずつ違うコピーを作りたいこともあります。

この場合、参照だけ共有していると、片方の変更がもう片方に影響してしまうことになります。

list・dict・オブジェクトとコピーの関係

Pythonでは、特に次のような「ミュータブル(変更可能)なオブジェクト」を扱うときに、コピーの理解が重要になります。

  • list(cst-code>[…])
  • dict(cst-code>{…})
  • set(cst-code>{1, 2, 3})
  • 自作クラスのインスタンス

一方で、次のような「イミュータブル(変更不能)なオブジェクト」は、コピーを深く意識しなくても問題が起こりにくいです。

  • int, float
  • str
  • tuple(中身がイミュータブルな場合)
  • frozenset など

特にlistとdictは、ネスト(入れ子)されて使われることが多いため、シャローコピーとディープコピーの差が大きく現れます。

シャローコピー(shallow copy)の仕組みと使い方

シャローコピーの定義と参照のコピー

シャローコピーとは、「一段目だけ新しく作り、中身の要素は元のオブジェクトを参照するコピー」のことです。

コードで表すと、次のようなイメージになります。

Python
import copy

a = [[1, 2], [3, 4]]
b = copy.copy(a)  # シャローコピー

print(a is b)          # 外側のリストは別物
print(a[0] is b[0])    # 内側のリストは同じものを参照している
実行結果
False
True

「is」演算子は同一オブジェクトかどうかを調べています。

この結果から、外側は別オブジェクト、中身は同一オブジェクトであることがわかります。

listのシャローコピー方法

listのシャローコピーは、いくつかの書き方があります。

どれも外側のリストだけ新規作成し、要素はそのまま参照する点で同じです。

代表的な3つの書き方

Python
import copy

lst = [1, 2, 3]

# 1. スライスでコピー
sh1 = lst[:]          # 全体スライス

# 2. listコンストラクタでコピー
sh2 = list(lst)

# 3. copyメソッドでコピー
sh3 = lst.copy()

# 4. copy.copyでコピー
sh4 = copy.copy(lst)

print(lst is sh1, lst is sh2, lst is sh3, lst is sh4)
実行結果
False False False False

これらはどれもシャローコピーです。

ネストしていない単純なlistであれば、実質的に「完全なコピー」として振る舞います。

ネストしている場合の挙動

同じシャローコピーでも、listの中にlistが入っている場合、違いがはっきりわかります。

Python
import copy

a = [[1, 2], [3, 4]]
b = a[:]        # シャローコピー
c = list(a)     # 同じくシャローコピー

b[0][0] = 99

print("a:", a)
print("b:", b)
print("c:", c)
print(a[0] is b[0], b[0] is c[0])
実行結果
a: [[99, 2], [3, 4]]
b: [[99, 2], [3, 4]]
c: [[99, 2], [3, 4]]
True True

外側のリストは別物ですが、中のリストを共有しているため、どれか1つを変更すると他も変わってしまうことがわかります。

dict・setのシャローコピー方法

listと同様に、dictやsetにもシャローコピーの方法が用意されています。

dictのシャローコピー

Python
import copy

d = {"user": {"name": "Alice", "age": 20}, "active": True}

# 1. dictのcopyメソッド
d1 = d.copy()

# 2. dictコンストラクタ
d2 = dict(d)

# 3. copy.copy
d3 = copy.copy(d)

print(d is d1, d is d2, d is d3)        # 外側は別
print(d["user"] is d1["user"])          # 内側のdictは同じ
実行結果
False False False
True

dictの中にdictやlistが入っていると、その内側は共有されたままになります。

setのシャローコピー

setも同様です。

ただしsetはネスト構造をあまり作らないため、シャローコピーでも問題になることは少ないです。

Python
import copy

s = {1, 2, 3}

s1 = s.copy()
s2 = set(s)
s3 = copy.copy(s)

print(s is s1, s is s2, s is s3)
実行結果
False False False

シャローコピーが有効なケースとメリット

シャローコピーには、次のようなメリットがあります。

コスト(時間・メモリ)が小さい

シャローコピーは、外側のコンテナだけを新しく作り、参照をコピーするだけなので、高速でメモリ消費も少なく済みます。

特に、次のような場合に向いています。

  • 中に大量のデータが入っているが、それ自体は変更しない場合
  • 内側のオブジェクトはイミュータブル(int, str, tupleなど)で構成されている場合
  • 関数内で外側のリストやdict自体を差し替えたいが、中身は触らない場合

イミュータブルな要素だけを持つ場合は「事実上の完全コピー」

例えば[1, 2, 3]["a", "b", "c"]など、内側にミュータブルな要素が入っていない場合、シャローコピーしても元のオブジェクトと独立して扱えます。

「中身を変更しない」か「中身がそもそも変更不能」なら、シャローコピーを積極的に使うと効率的です。

シャローコピーで起こる思わぬ副作用の例

シャローコピーで一番多いトラブルは、「ネストされた中身までコピーされた」と勘違いして書き換えてしまうケースです。

ネストしたlistでの事故例

Python
import copy

original = [[0] * 3 for _ in range(3)]
shallow = copy.copy(original)  # シャローコピー

shallow[0][0] = 99

print("original:", original)
print("shallow :", shallow)
実行結果
original: [[99, 0, 0], [0, 0, 0], [0, 0, 0]]
shallow : [[99, 0, 0], [0, 0, 0], [0, 0, 0]]

このように、外側だけ別で中身を共有しているため、内側の要素を書き換えると両方に影響が出ることがあります。

dictの中のdictでも同様

Python
import copy

config = {
    "db": {"host": "localhost", "port": 5432},
    "debug": True,
}

cfg_copy = config.copy()  # シャローコピー

cfg_copy["db"]["host"] = "example.com"

print("config:", config)
print("copy  :", cfg_copy)
実行結果
config: {'db': {'host': 'example.com', 'port': 5432}, 'debug': True}
copy  : {'db': {'host': 'example.com', 'port': 5432}, 'debug': True}

設定を少し変えたコピーを作ったつもりが、元の設定まで書き換えてしまう、というよくある失敗パターンです。

ディープコピー(deep copy)の仕組みと使い方

ディープコピーの定義と再帰的コピー

ディープコピーとは、オブジェクトの中身を再帰的にたどって、新しいオブジェクトをすべて作り直すコピーです。

ディープコピーを行うと、どこを変更しても元のオブジェクトには影響しないようになります。

その代わり、処理時間とメモリ消費はシャローコピーより大きくなります

copyモジュール(deepcopy)の基本的な使い方

Python標準ライブラリのcopyモジュールに、ディープコピーのためのdeepcopy関数が用意されています。

Python
import copy

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

b = copy.deepcopy(a)  # ディープコピー

print(a is b)           # 外側は別
print(a[0] is b[0])     # 内側のリストも別
実行結果
False
False

シャローコピー(copy.copy)とディープコピー(copy.deepcopy)を使い分けるのが基本的なスタイルです。

ネストしたlist・dict・オブジェクトのディープコピー例

ネストしたlistのディープコピー

先ほどの副作用例を、ディープコピーで書き直してみます。

Python
import copy

original = [[0] * 3 for _ in range(3)]
deep = copy.deepcopy(original)  # ディープコピー

deep[0][0] = 99

print("original:", original)
print("deep    :", deep)
print(original[0] is deep[0])
実行結果
original: [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
deep    : [[99, 0, 0], [0, 0, 0], [0, 0, 0]]
False

内側のlistも別オブジェクトになっているので、書き換えても元のデータは完全に保護されます。

ネストしたdictのディープコピー

Python
import copy

config = {
    "db": {"host": "localhost", "port": 5432},
    "debug": True,
}

cfg_deep = copy.deepcopy(config)

cfg_deep["db"]["host"] = "example.com"

print("config:", config)
print("deep  :", cfg_deep)
print(config["db"] is cfg_deep["db"])
実行結果
config: {'db': {'host': 'localhost', 'port': 5432}, 'debug': True}
deep  : {'db': {'host': 'example.com', 'port': 5432}, 'debug': True}
False

元の設定はまったく変わらないため、テンプレート的な設定から派生設定を作るときなどに安心して使えます。

自作クラスインスタンスのディープコピー

deepcopyは、自作クラスのインスタンスについても、内部の属性をたどりながらコピーしてくれます。

Python
import copy

class User:
    def __init__(self, name, tags):
        self.name = name      # str(イミュータブル)
        self.tags = tags      # list(ミュータブル)

user1 = User("Alice", ["admin", "dev"])
user2 = copy.deepcopy(user1)

user2.tags.append("tester")

print("user1.tags:", user1.tags)
print("user2.tags:", user2.tags)
print(user1.tags is user2.tags)
実行結果
user1.tags: ['admin', 'dev']
user2.tags: ['admin', 'dev', 'tester']
False

インスタンス内のミュータブルな属性も別オブジェクトになるため、インスタンス間で状態が混線することを防げます。

ディープコピーのメリットと注意点

メリット

ディープコピーの最大のメリットは、元オブジェクトとの完全な独立性です。

ネストがどれだけ深くても、あるいは自作クラスが入り組んでいても、コピー後のオブジェクトを安心して変更できます。

  • 設定や状態の「スナップショット」を安全に残せる
  • テンプレートから派生オブジェクトを作るときに安全
  • 並列処理やバックグラウンド処理用に状態を渡すときに安心

注意点(欠点)

一方で、ディープコピーには次のような注意点があります。

  • 処理が重い
    構造が大きく複雑な場合、再帰的なコピーは時間もメモリも多く消費します。
  • すべてがコピーできるとは限らない
    ファイルオブジェクト、データベース接続、スレッド、ロックなど、コピーすべきでない(あるいはできない)オブジェクトを含む場合、deepcopyは例外を出すことがあります。
  • 特殊なクラスでは挙動をカスタマイズしないと期待通りにならないことがある
    __deepcopy__メソッドを実装して制御することも可能ですが、その場合は設計が必要です。

シャローコピーvsディープコピーの使い分け

Pythonでのシャローコピーとディープコピーの挙動比較

まずは、シャローコピーとディープコピーの挙動を1つのコードで比較してみます。

Python
import copy

data = {
    "users": [
        {"name": "Alice", "age": 20},
        {"name": "Bob", "age": 25},
    ]
}

shallow = copy.copy(data)
deep = copy.deepcopy(data)

# シャローコピー側を変更
shallow["users"][0]["age"] = 99

print("original:", data)
print("shallow :", shallow)
print("deep    :", deep)
実行結果
original: {'users': [{'name': 'Alice', 'age': 99}, {'name': 'Bob', 'age': 25}]}
shallow : {'users': [{'name': 'Alice', 'age': 99}, {'name': 'Bob', 'age': 25}]}
deep    : {'users': [{'name': 'Alice', 'age': 20}, {'name': 'Bob', 'age': 25}]}

この例から、シャローコピーは「一部共有」、ディープコピーは「完全分離」という違いが視覚的にも理解できます。

ネスト構造の有無による使い分けの判断基準

実務でコピー方法を選ぶときは、次の観点で考えると整理しやすいです。

1. ネストしているかどうか

  • ネストなし(例: [1, 2, 3]{"a": 1, "b": 2})
    → シャローコピーで十分なことが多い。
  • ネストあり(例: [[...], [...]]{"users": [{"...": ...}]})
    内側を変更する可能性があるならディープコピーを検討

2. 中身がミュータブルかどうか

  • 中身がイミュータブルのみ
    → 共有しても問題が出にくいので、シャローコピーでOK。
  • 中身にlistやdict、自作クラスなどミュータブルが含まれる
    → 内側を変更するならディープコピー。

3. 変更の範囲

  • 外側だけ差し替えたり、要素の追加・削除だけを行い、内側のオブジェクト自体は触らない
    → シャローコピーが有効。
  • 内側のオブジェクトの属性を書き換えたり、中のリストに追加したりする
    → ディープコピーを使うか、あるいはピンポイントに新しいオブジェクトを作る。

パフォーマンスとメモリから見るコピー戦略

ディープコピーは安全ですが、その分コストが高いという現実があります。

特に大きなデータ構造を頻繁にコピーする場合、パフォーマンスへの影響が無視できなくなります。

パフォーマンス面

  • シャローコピー
    • 外側のコンテナの長さに比例する程度の軽い処理。
    • 大量の要素があっても、「参照のコピー」だけなので比較的高速。
  • ディープコピー
    • 構造全体を再帰的にたどるため、ネストした深さや要素数にほぼ比例してコストが増大。
    • 大規模なデータ(数万〜数十万要素以上)を頻繁にコピーすると、明確なボトルネックになり得る。

メモリ面

  • シャローコピー
    • 外側のコンテナ分しか増えないため、メモリ負荷は小さい。
  • ディープコピー
    • 構造全体を2倍にすることになるため、大きなオブジェクトではメモリを大きく消費する。

「とりあえず全部deepcopy」は危険で、特にサーバーアプリケーションや長時間動作するスクリプトでは、必要な部分だけをコピーする工夫が必要です。

実務でのベストプラクティスとよくあるミス対策

ベストプラクティスの例

  1. まず「コピーが本当に必要か」を考える
    1. 参照を共有しても安全なケースなら、あえてコピーしない方がシンプルで効率的です。
  2. ネスト構造を意識し、必要最小限のコピー方法を選ぶ
    1. ネストしていない・中身がイミュータブルならシャローコピー。
    2. ネストしていて内側も変更するならディープコピー、あるいは部分的な新規生成。
  3. 設定テンプレートや初期データはイミュータブルに寄せる
    1. 可能であればtupleやfrozensetを活用し、「そもそも変更できない」構造を使うとトラブルが減ります。
  4. 関数内で入力引数を破壊的変更しない
    1. 書き換えが必要な場合は、明示的にコピーしてから操作するか、新しいオブジェクトを返すスタイルを採用します。

よくあるミスと対策

  1. 代入をコピーだと勘違いする
    1. 対策: a = bは「別名をつけているだけ」と意識し、コピーしたいときは明示的にcopy()deepcopy()を使う。
  2. シャローコピーでネストされた中身までコピーされたと思い込む
    import copy
    
    a = [[1, 2], [3, 4]]
    b = copy.copy(a)
    
    print(a[0] is b[0])  # Trueなら中身は共有されている
    
    1. 対策: ネスト構造を扱うときは、isでオブジェクトの同一性を確認してみるクセをつける。
  3. パフォーマンスやメモリを考えずにdeepcopyを乱用する
    1. 対策: ボトルネックになっていないか計測し、必要に応じて部分的なコピーやアーキテクチャの見直しを行う。
  4. コピーできない(またはすべきでない)オブジェクトをdeepcopyしようとする
    1. 対策: ファイル、接続、スレッドなどはコピーせず、必要ならハンドルやIDだけを持つ設計に切り替える。

まとめ

Pythonのシャローコピーとディープコピーは、どちらが優れているというものではなく、用途に応じて使い分けるべき道具です。

シャローコピーは外側だけを新しく作り中身を共有するため、高速でメモリ効率に優れますが、ネスト構造では思わぬ副作用が起こり得ます。

一方、ディープコピーは構造全体を再帰的にコピーして完全に独立したオブジェクトを作るため、安全性は高いものの、コストも大きくなります。

実務では、データ構造のネストの有無や中身のミュータブル性、変更の範囲を意識しながら、「本当にコピーが必要な部分だけを、適切な方法でコピーする」という姿勢を持つことが、バグの少ない効率的なコードにつながります。

コーディングテクニック

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!