辞書やリストをコピーしたのに、変更前のデータまで変わることがある。原因は、外側の入れ物だけをコピーし、その中のリストや辞書を共有しているためである。入れ子の測定条件を独立させたいなら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固有のコマンドや環境ごとに変わるパスは、本文中の条件を確認して使ってほしい。
