Python | データ構造強化:ディープコピー(copy.deepcopy)

Python Python
スポンサーリンク

概要(ディープコピーは「入れ物も中身も全部“別物”にする」コピー)

copy.deepcopy は、リストや辞書などの入れ子構造を含むオブジェクトを「最上位から末端まで」再帰的にコピーして、完全に独立した複製を作ります。代入「=」は参照共有、シャローコピーは外側だけ独立、ディープコピーは内側まで独立。この違いを体で覚えると、予期せぬ連動バグを防げます。

import copy

a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)
b[0].append(99)
print(a)  # [[1, 2], [3, 4]](影響なし)
print(b)  # [[1, 2, 99], [3, 4]]
Python

基本の使い方(ここが重要)

ディープコピーの基本形と「=/シャロー」との違い

import copy

a = [[1], [2]]
x = a                    # 代入(参照共有)
y = a.copy()             # シャローコピー(外側だけ別)
z = copy.deepcopy(a)     # ディープコピー(内側まで別)

y[0].append(9)
z[1].append(8)

print(a)  # [[1, 9], [2]] ← y の変更が波及(内側共有)
print(z)  # [[1], [2, 8]] ← 完全独立
Python

イミュータブルは“見かけ上”問題になりにくい

数値・文字列・タプルなどは中身を変更できないため、シャローでも連動問題が表面化しにくい一方、辞書やリストなどミュータブル要素が入っていると連動します。構造の“どこがミュータブルか”を把握して使い分けましょう。

import copy
a = {"name": "hanako", "tags": ["coffee", "tea"]}
b = copy.deepcopy(a)
b["tags"].append("sugar")
print(a["tags"])  # ['coffee', 'tea'](影響なし)
Python

重要ポイントの深掘り(再帰コピーの仕組み・性能・制約)

deepcopy の再帰とメモ(循環参照にも対応)

deepcopy はオブジェクトを辿りながら再帰的にコピーします。循環参照(自分を参照する構造)があっても内部の「メモ」表で辿ったオブジェクトを覚えているため、無限ループになりません。

import copy

a = []
a.append(a)      # a 自身を要素に持つ(循環)
b = copy.deepcopy(a)
print(b is b[0]) # True(循環構造が保たれつつ“別物”)
Python

パフォーマンスの勘所(重いので“必要最小限”に)

  • コスト感: 構造のサイズ・深さに比例して時間とメモリが増える。
  • 指針: 変更が及ぶ可能性がある“必要な階層のみ”を手動コピー(防御的コピー)する方が軽く済むケースが多い。完全分離が必須なら deepcopy。
# 例:一次入れ子だけ独立させる(軽量防御)
a = [[1, 2], [3, 4]]
b = [row[:] for row in a]  # ここだけ守る
Python

コピーできない/すべきでないもの(制約)

  • ファイルハンドル・ソケット・スレッドロック: “そのもの”を複製しても意味がなく、例外や不正な状態になり得る。参照を保持し直す/ハンドルを再作成する設計にする。
  • 関数やクラスオブジェクト: 参照だけで十分なことが多い。必要に応じて設定データだけ deepcopy する。

ユーザー定義クラスの制御(deepcopy と memo)

自作クラスで「どうコピーされたいか」を定義できます。memo は循環回避用の辞書。ここで“必要部分だけ再生成”や“共有してよい部分はそのまま”など、意図をコード化できます。

import copy

class Config:
    def __init__(self, path, cache):
        self.path = list(path)
        self.cache = cache  # 重い共有資源は共有のままにする例

    def __deepcopy__(self, memo):
        new = type(self)(
            copy.deepcopy(self.path, memo),  # ここは独立
            self.cache                       # ここは共有
        )
        memo[id(self)] = new
        return new
Python

実務の使いどころ(テンプレート・スナップショット・安全化)

設定テンプレートを複製して加工(“元”を汚さない)

import copy

TEMPLATE = {
    "path": ["in", "out"],
    "opts": {"mode": "fast", "retry": 2},
}
cfg = copy.deepcopy(TEMPLATE)
cfg["path"].append("tmp")
cfg["opts"]["retry"] = 3
# TEMPLATE は不変
Python

スナップショットを取って比較(差分検出)

処理前後のデータ構造を独立に保持して差分を計算。ログ・検証・ロールバックに有効。

import copy

before = get_state()
after  = copy.deepcopy(before)
mutate(after)
diff = compute_diff(before, after)
Python

関数内での“防御的”完全コピー(外部を守る)

外部から受け取った構造を破壊的に加工する必要があるなら、最初に deepcopy。呼び出し側のデータを汚さない。

import copy

def transform(doc: dict) -> dict:
    w = copy.deepcopy(doc)
    # ここから自由に変更
    w.setdefault("meta", {})["processed"] = True
    return w
Python

例題で身につける(定番から一歩先まで)

例題1:シャロー vs ディープの違いを体感

import copy

a = {"items": [[1], [2]]}
shallow = a.copy()
deep    = copy.deepcopy(a)

shallow["items"][0].append(9)
deep["items"][1].append(8)

print(a)       # {'items': [[1, 9], [2]]} ← シャローで波及
print(deep)    # {'items': [[1], [2, 8]]} ← ディープは独立
Python

例題2:循環参照を含む構造のコピー

import copy

node = {}
node["self"] = node
clone = copy.deepcopy(node)
print(node is node["self"])   # True(元構造)
print(clone is clone["self"]) # True(複製側も循環維持)
print(clone is node)          # False(別物)
Python

例題3:部分的に共有を残す deepcopy 実装

import copy

class Cache: pass

class Model:
    def __init__(self, data, cache):
        self.data = data
        self.cache = cache

    def __deepcopy__(self, memo):
        new = type(self)(copy.deepcopy(self.data, memo), self.cache)
        memo[id(self)] = new
        return new

m1 = Model({"nums": [1,2,3]}, Cache())
m2 = copy.deepcopy(m1)
m2.data["nums"].append(4)
print(m1.data["nums"])  # [1,2,3](独立)
print(m1.cache is m2.cache)  # True(共有)
Python

例題4:巨大構造は“必要階層だけ”コピーして軽量化

# 全体 deepcopy は重い → 編集対象ブロックだけ独立にする
doc = {"header": {"ts": 0}, "body": [{"id": 1}, {"id": 2}]}
body = [row.copy() for row in doc["body"]]  # ここだけコピー
body[0]["id"] = 100
print(doc["body"][0]["id"])  # 1(影響なし)
Python

まとめ

deepcopy は「外側も内側もすべて別物」にする完全コピーで、入れ子のミュータブル要素を安全に独立させます。= は参照共有、シャローコピーは外側のみ独立、deepcopy は末端まで独立。循環参照にも対応しますが、コストは重いため“必要最小限の階層コピー”との使い分けが鍵。コピー不適切な資源(ファイル・ソケット等)は再作成や共有設計を選び、必要なら deepcopy でポリシーを明文化する。テンプレート複製、スナップショット、関数内防御など、場面ごとに最短で“安全”を確保する癖をつけましょう。

タイトルとURLをコピーしました