【Python】コピーしたデータまで変わる?浅いコピーと深いコピーを使い分ける

PythonのTopに戻る


辞書やリストをコピーしたのに、変更前のデータまで変わることがある。原因は、外側の入れ物だけをコピーし、その中のリストや辞書を共有しているためである。入れ子の測定条件を独立させたいならcopy.deepcopy()、変更する場所が決まっているならその部分だけのコピーを使う。まずは、代入・浅いコピー・深いコピーの違いを同じデータで確認する。

代入では新しいデータは作られない

例えば測定条件をoriginalに保存し、trial = originalとして試行用の名前を付けても、二つの名前は同じ辞書を指している。trialの中身を変更するとoriginalにもその変更が見える。これはコピーの失敗ではなく、代入がオブジェクトへの参照を渡す操作だからである。

一方、dict.copy()は外側の辞書を新しく作る。ただし、値として格納されたリストや別の辞書までは複製しない。最上位の数値や文字列を別の値に置き換えるだけなら困らないが、内側のリストにappendする場合には注意が必要になる。

浅いコピーと深いコピーを比較する

以下は、試料名と走査条件を持つ辞書の例である。まず浅いコピーの内側を書き換え、次にdeepcopyで独立した条件を作る。isは「値が同じか」ではなく「同じオブジェクトか」を調べるために使っている。

example.py

from copy import deepcopy

original = {"sample": "A", "scan": {"steps": [1, 2]}}
alias = original
shallow = original.copy()
print(alias is original, shallow is original)
print(shallow["scan"] is original["scan"])

shallow["scan"]["steps"].append(3)
print(original["scan"]["steps"])
assert original["scan"]["steps"] == [1, 2, 3]

deep = deepcopy(original)
deep["scan"]["steps"].append(4)
print(original["scan"]["steps"], deep["scan"]["steps"])
assert deep["scan"] is not original["scan"]
assert original["scan"]["steps"] == [1, 2, 3]

selected = {
    **original,
    "scan": {**original["scan"], "steps": original["scan"]["steps"].copy()},
}
selected["scan"]["steps"].append(5)
print(selected["scan"]["steps"])
assert original["scan"]["steps"] == [1, 2, 3]

実行結果

True False
True
[1, 2, 3]
[1, 2, 3] [1, 2, 3, 4]
[1, 2, 3, 5]

shallowはoriginalと別の辞書だが、scanは共通である。そのため浅いコピーからstepsを追加すると元のstepsも増える。一方、deep側への追加は元に反映されない。コピーする前にすでに行われた変更は、deepcopyで取り消せない点にも注意したい。

変更する部分だけをコピーする

最後のselectedでは、外側の辞書・scan辞書・stepsリストをそれぞれ作り直している。この三か所が変更対象までの経路である。経路の途中に共有した辞書が残ると、その辞書への代入は元にも伝わる。内側のリストだけ新しくすればよい、と一段だけ見て判断しないことが大切である。

測定配列のように大きな値を変更せず使う場合、すべてをdeepcopyすると不要な複製が増えることもある。設定の小さな枝だけを変更したいなら、その枝をコピーする方法は意図が読み取りやすい。ただし、共有して残した部分を後で変更しないという前提が必要になる。

どの方法を選べばよいか

複製方法と共有範囲
方法 外側 入れ子の可変データ
代入 共有 共有
dict.copy() / copy.copy() 新しい入れ物 原則として共有
copy.deepcopy() 新しい入れ物 対応する型を再帰的に複製
必要部分だけコピー 指定した範囲を複製 残した箇所は共有

deepcopyでも、ファイル、ソケット、外部接続などを新しい独立した資源へ変換できるわけではない。独自クラスではコピーの動作を定義することもできる。何でも独立させる万能な保存処理とは考えず、まず通常の辞書・リスト・数値・文字列で構成されたデータに使うと理解しやすい。

同じ内側のオブジェクトを複数の場所から参照する構造では、deepcopyは通常、その共有関係をコピー先の中でも保つ。各要素を完全に別々のものにしたいという要求とは異なる。オブジェクト同士の関係まで含めて、何を独立させたいかを決めておく必要がある。

変更後の値と共有関係を確かめる

不具合の確認では、印字した辞書が同じに見えることだけで判断しない。元を変更したときのコピー先、コピー先を変更したときの元の両方を確認する。重要な枝についてisで共有の有無を調べると、意図しないつながりを見つけやすい。

関数へ渡した辞書の中身が変わる場合にも考え方は同じである。呼び出し側のデータを更新する関数なのか、コピーして結果を返す関数なのかを決めておこう。コピーの深さと、関数がどこを変更するかをそろえることが、元データを保つための基本になる。

動作確認と参考資料

掲載例はLinux・CPython 3.12.14で動作確認した。OS固有のコマンドや環境ごとに変わるパスは、本文中の条件を確認して使ってほしい。

関連するTips


PythonのTopに戻る