Python | データ構造:コレクションのコピー

Python
スポンサーリンク

概要(「代入」と「コピー」をまず分けて考える)

コレクションのコピーは、元データを壊さず安全に加工するための基本です。まず重要なのは「= はコピーではない」ということ。= は“同じオブジェクトへの参照を共有”するだけで、片方を変更するともう片方も変わります。コピーには大きく「浅いコピー(shallow copy)」と「深いコピー(deep copy)」があり、用途によって使い分けます。

original = [1, 2, 3]
alias = original        # 参照共有(コピーではない)
alias[0] = 99
print(original)  # [99, 2, 3] ← 影響を受ける
Python

代入・浅いコピー・深いコピー(ここが重要)

代入(参照共有)

代入は“同じものを別名で呼ぶ”だけ。高速ですが、独立させたい場面では使いません。誤用がバグの温床になります。

浅いコピー(1階層だけ複製)

浅いコピーは“外側のコンテナ”だけ新しく作り、中に入っている要素(参照)はそのまま共有します。要素が不変(int/str/tuple)なら十分ですが、要素がリストや辞書など“ミュータブル”だと中身の変更が共有されます。

import copy

a = [[1, 2], [3, 4]]
b = copy.copy(a)   # a[:], list(a), a.copy() と同等(浅いコピー)
b[0][0] = 99
print(a)  # [[99, 2], [3, 4]] ← 内側が共有されている
Python

深いコピー(入れ子の中まで完全複製)

deepcopy は入れ子のミュータブル要素まで“辿って”新しく作り、元データと完全に独立させます。安全ですが、コスト(時間・メモリ)が高め。必要な場面だけ使いましょう。

import copy

a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)
b[0][0] = 99
print(a)  # [[1, 2], [3, 4]] ← 影響なし(完全に独立)
Python

コレクション別のコピー方法(一発で迷わない実用リスト)

リスト(list)

  • 浅いコピー: a[:]、list(a)、a.copy()
  • 深いコピー: copy.deepcopy(a)
a = [1, 2, 3]
shallow = a[:]          # または a.copy()
deep    = __import__("copy").deepcopy(a)
Python

辞書(dict)

  • 浅いコピー: a.copy()、dict(a)
  • 深いコピー: copy.deepcopy(a)
  • 注意: dict.keys()/values()/items() は“ビュー”でありコピーではありません(元の辞書変更が反映されます)。
a = {"name": "coffee", "info": {"price": 350}}
shallow = a.copy()
deep    = __import__("copy").deepcopy(a)
Python

セット(set)

  • 浅いコピー: a.copy()、set(a)
  • 深いコピー: copy.deepcopy(a)(内側にミュータブルが入っているタプルなどを含む場合のみ意味あり)
a = {1, 2, 3}
shallow = a.copy()
Python

タプル(tuple)

  • タプル自体は“不変”なのでコピーの必要は基本的にありません。同じ参照を共有して問題ありません。
  • ただし“中にミュータブルが入っているタプル”は、deepcopy しないと内側が共有されます。
import copy
a = (1, [2, 3])
b = copy.deepcopy(a)
b[1][0] = 99
print(a)  # (1, [2, 3])
Python

よくある落とし穴と安全策(重要ポイントを深掘り)

「ビュー」と「コピー」を混同しない

dict.keys()/values()/items() はコピーではなく“現在の内容を映すビュー”。元辞書の更新が反映されます。値を固定したいときは list(…) などで“その時点のスナップショット”を作ります。

d = {"a": 1}
v = d.values()       # ビュー
d["b"] = 2
print(list(v))       # [1, 2] ← 反映される
Python

ネスト構造は浅いコピーだと“中身が共有”される

内側のリスト・辞書に手を入れる予定があるなら、deepcopy 一択。浅いコピーで十分なのは「内側を変更しない前提」か「内側が不変オブジェクト」の場合です。

コストと必要性で選ぶ(深いコピーは最後の手段)

deepcopy は“全部辿る”ため重くなりがち。代替として:

  • 変更が必要な部分だけを新規作成する(部分的 deepcopy)
  • データ設計を“不変(イミュータブル)中心”にする(タプル、frozenset)でコピー自体を減らす
  • 共有して良い部分は浅いコピー+注意書き(コメント)で運用

カスタムクラスのコピー

自作クラスは copy.copy()/copy.deepcopy() で動きますが、特殊なリソース(ファイル、ロック)を持つ場合は、copy/deepcopy を実装して“どう複製するか”を定義すると安全です。

import copy

class Box:
    def __init__(self, items):
        self.items = items
    def __copy__(self):
        return Box(self.items.copy())
    def __deepcopy__(self, memo):
        return Box(copy.deepcopy(self.items, memo))
Python

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

例題1:設定テンプレートから安全に派生

import copy

TEMPLATE = {
    "db": {"host": "localhost", "port": 5432},
    "features": {"beta": False},
}

conf = copy.deepcopy(TEMPLATE)
conf["features"]["beta"] = True
print(TEMPLATE["features"]["beta"])  # False(元に影響なし)
Python

例題2:浅いコピーで十分なケース(内側不変)

a = [{"id": 1, "tags": ("coffee", "drink")}, {"id": 2, "tags": ("tea",)}]
b = a[:]  # 外側だけ新しく、内側のタプルは不変なので安全
b[0] = {"id": 1, "tags": ("bean",)}  # 外側の差し替え
print(a[0]["tags"])  # ('coffee', 'drink')(内側に影響なし)
Python

例題3:ビューとスナップショットの違い

d = {"coffee": 350}
vals_view = d.values()     # ビュー
vals_list = list(d.values())  # スナップショット

d["tea"] = 280
print(list(vals_view))  # [350, 280](反映)
print(vals_list)        # [350](固定)
Python

例題4:部分的 deepcopy でコスト削減

import copy

a = {"meta": {"ver": 1}, "data": [{"x": 1}, {"x": 2}]}
b = a.copy()  # 浅いコピー
b["data"] = copy.deepcopy(a["data"])  # 変更予定の部分だけ深く
b["data"][0]["x"] = 99
print(a["data"][0]["x"])  # 1(元は守られる)
Python

まとめ

  • = はコピーではなく参照共有。独立させたいなら必ずコピーを使う。
  • 浅いコピーは“外側だけ新規”、入れ子のミュータブルは共有される。a[:], a.copy(), list(a)/dict(a)/set(a)。
  • 深いコピーは入れ子の中まで複製。copy.deepcopy は安全だが重いので“必要な場面だけ”。
  • dict の keys/values/items はビュー。固定したいときは list(…) でスナップショット。
  • 設計で“不変を増やす”“部分的 deepcopy”を選ぶと、性能と安全性のバランスが取れる。

この判断軸を身につければ、初心者でも「壊さずに加工する」安全なコードが書けます。

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