閉じる

もうハマらない! list.copyとcopy.deepcopyの正しい使い分け

「コピーしたのに元のリストまで変わってしまった」そんな経験は、Pythonのリストが参照を共有する性質や、シャローコピーとディープコピーの違いを理解すれば防げます。

本記事では、ハマりどころを段階的に解説し、list.copycopy.deepcopyの正しい使い分けを実例付きで説明します。

Pythonのリストのコピーでハマる理由

Pythonのリストはミュータブル(変更可能)なオブジェクトです。

コピー方法を誤ると、思わぬ箇所で値が連動してしまいます。

ここでは、よくある勘違いの根本原因を整理します。

代入は参照共有でコピーではない

Pythonでb = aと書くと、新しいリストが作られるわけではありません。

変数abが同じリストオブジェクトを参照します。

Python
# 代入はコピーではなく、同じオブジェクトを指す参照共有です
a = [1, 2, 3]
b = a          # b は a と同じリストを参照

b[0] = 99      # b からの変更が a にも見える
print("a:", a)
print("b:", b)
print("同じオブジェクトか:", a is b)  # True なら同一オブジェクト
実行結果
a: [99, 2, 3]
b: [99, 2, 3]
同じオブジェクトか: True

ミュータブルとイミュータブルの違い

リストや辞書はミュータブル、整数や文字列、タプルはイミュータブルです。

ミュータブルは「中身をその場で変更」でき、イミュータブルは「新しいオブジェクトを作って置き換え」ます。

この違いが、コピー時の挙動に影響します。

Python
# イミュータブル(int)の変更は新しいオブジェクトの再代入になる
nums = [1, 2, 3]
copy_nums = nums.copy()  # シャローコピー
copy_nums[0] += 10       # イミュータブルなので新しい int を作って代入
print("nums:", nums)
print("copy_nums:", copy_nums)

# ミュータブル(リスト)はその場で変更される
nested = [[1, 2], [3, 4]]
copy_nested = nested.copy()  # シャローコピー
copy_nested[0][0] = 99       # ネスト内(ミュータブル)をその場で変更
print("nested:", nested)
print("copy_nested:", copy_nested)
実行結果
nums: [1, 2, 3]
copy_nums: [11, 2, 3]
nested: [[99, 2], [3, 4]]
copy_nested: [[99, 2], [3, 4]]

ネストで変更が伝播する

ネストされたリストや辞書では、上位の容器だけをコピーしても、内側の要素が共有され続けるため変更が伝播します。

Python
# 上位だけコピーしても、内側は共有される典型例
config = [{"retry": 3}, {"timeout": 5}]
shallow = config.copy()  # シャローコピー

# 内側の辞書を書き換えると、両方に反映される
shallow[0]["retry"] = 10
print("config:", config)
print("shallow:", shallow)
実行結果
config: [{'retry': 10}, {'timeout': 5}]
shallow: [{'retry': 10}, {'timeout': 5}]

シャローコピーの基礎

シャローコピーは、最上位のリストだけ新しく作り直し、内側の要素参照はそのまま共有します。

軽量で高速ですが、ネスト構造では注意が必要です。

list.copyの効果と限界

list.copy()は最上位のリストのみを複製します。

内側のオブジェクトは同じ参照を保持します。

Python
# list.copy は最上位のリストを新規に作るが、要素参照は共有
a = [[0], [1], [2]]
b = a.copy()

print("a is b:", a is b)                       # 別オブジェクト
print("a[0] is b[0]:", a[0] is b[0])           # 内側は同じ参照

b.append([3])                                  # 上位リストの変更は独立
b[0].append(99)                                # ただし内側の要素は共有のため伝播
print("a:", a)
print("b:", b)
実行結果
a is b: False
a[0] is b[0]: True
a: [[0, 99], [1], [2]]
b: [[0, 99], [1], [2], [3]]

スライスやcopy.copyとの関係

a[:]copy.copy(a)も、リストに対してはlist.copy()と同じくシャローコピーです。

可読性や一貫性の観点で、チーム規約に合わせて選ぶとよいです。

Python
import copy

a = [1, 2, 3]
b = a[:]           # スライスでシャローコピー
c = a.copy()       # メソッドでシャローコピー
d = copy.copy(a)   # copy.copy でもシャローコピー

print(a is b, a is c, a is d)  # いずれも別オブジェクト
print(a == b == c == d)
実行結果
False False False
True

ネスト要素は共有される

ネスト構造では、シャローコピーでは不十分です。

内側の参照が共有されるため、内側の変更が元のリストに影響します。

Python
import copy

a = [{"users": ["alice", "bob"]}]
b = copy.copy(a)    # または a[:] や a.copy()

b[0]["users"].append("carol")  # ネスト内を変更
print("a:", a)
print("b:", b)
実行結果
a: [{'users': ['alice', 'bob', 'carol']}]
b: [{'users': ['alice', 'bob', 'carol']}]

ディープコピーの基礎

ディープコピーは、ネストされたオブジェクトを再帰的に複製します。

内側のミュータブル要素も独立するため、安全に変更できます。

ネストを含めて再帰的に複製

copy.deepcopyは、可能な限り全階層で新しいオブジェクトを作ります。

Python
import copy

a = [{"users": ["alice", "bob"]}]
d = copy.deepcopy(a)           # すべての階層を複製

d[0]["users"].append("carol")  # 変更は a に伝播しない
print("a:", a)
print("d:", d)
実行結果
a: [{'users': ['alice', 'bob']}]
d: [{'users': ['alice', 'bob', 'carol']}]

循環参照とパフォーマンスの注意

deepcopyは循環参照を検出して安全に処理しますが、コストは高くなりがちです。

大きく深い構造では、必要な部分だけを個別にコピーする戦略も検討します。

コード例(循環参照)

Python
import copy

x = []
x.append(x)                  # 自分自身を参照する循環構造
y = copy.deepcopy(x)

print("x は自分自身を指すか:", x[0] is x)
print("y も自分自身を指すか:", y[0] is y)
print("x と y は別物か:", x is not y)
実行結果
x は自分自身を指すか: True
y も自分自身を指すか: True
x と y は別物か: True

コード例(負荷の目安)

Python
# 注意: 実行環境で時間は変わります。相対比較の目安です。
import copy, time

data = [{"vals": [i for i in range(1000)]} for _ in range(1000)]

t0 = time.perf_counter()
_ = data.copy()          # シャローコピー
t1 = time.perf_counter()
_ = copy.deepcopy(data)  # ディープコピー
t2 = time.perf_counter()

print("shallow:", format(t1 - t0, ".7f"), "seconds")
print("deep   :", format(t2 - t1, ".7f"), "seconds")
実行結果
shallow: 0.0000057 seconds
deep   : 0.1253064 seconds

カスタムオブジェクトの扱い

ユーザー定義クラスは、__copy____deepcopy__を実装して挙動を制御できます。

デフォルトでは、deepcopyは属性をたどって複製しますが、外部リソースやキャッシュなどは再利用したい場合があります。

Python
import copy

class Node:
    def __init__(self, name, children=None, cache=None):
        self.name = name
        self.children = list(children or [])
        self.cache = cache  # 例: 重いキャッシュは共有したいとする

    def __copy__(self):
        # シャローコピー: 子は同じ参照を共有、cache も共有
        new_obj = type(self)(self.name, self.children, self.cache)
        return new_obj

    def __deepcopy__(self, memo):
        # ディープコピー: 子は再帰的にコピー、cache は共有のままにする(方針の例)
        if id(self) in memo:
            return memo[id(self)]
        new_obj = type(self)(self.name, cache=self.cache)
        memo[id(self)] = new_obj
        new_obj.children = [copy.deepcopy(c, memo) for c in self.children]
        return new_obj

    def __repr__(self):
        return f"Node({self.name!r}, children={self.children}, cache={self.cache!r})"

root = Node("root", [Node("a"), Node("b")], cache={"heavy": True})
shallow = copy.copy(root)
deep = copy.deepcopy(root)

# 変更の影響範囲を確認
shallow.children[0].name = "A*"
deep.children[1].name = "B*"

print("root:", root)
print("shallow:", shallow)
print("deep:", deep)
print("cache 共有確認:", root.cache is shallow.cache, root.cache is deep.cache)
実行結果
root: Node('root', children=[Node('A*', children=[], cache=None), Node('b', children=[], cache=None)], cache={'heavy': True})
shallow: Node('root', children=[Node('A*', children=[], cache=None), Node('b', children=[], cache=None)], cache={'heavy': True})
deep: Node('root', children=[Node('a', children=[], cache=None), Node('B*', children=[], cache=None)], cache={'heavy': True})
cache 共有確認: True True

list.copyとcopy.deepcopyの正しい使い分け

どちらを選ぶかは「ネストのどのレベルまで独立させたいか」で決まります。

性能と安全性のバランスを考え、必要十分なコピーを選択します。

判断基準の要点

  • ネストがない、または内側を変更しない前提ならlist.copyで十分です。単純な並べ替えやスライス取得などに向いています。
  • 内側の要素を編集する可能性があるならcopy.deepcopyを選びます。チーム開発では特に安全側に倒した方が不具合を防げます。
  • 大規模データではコストに注意します。ディープコピーの対象を最小化するか、必要な部分だけ個別にコピーします。
  • 代入b = aはコピーではありません。明示的にコピー関数を使い、意図をコードで表現します。

よくある実例とベストプラクティス

設定やJSONライクなデータ、リストのリストなど、日常的によく遭遇する例で比較します。

例1: 単純な複製と並べ替え

Python
# トップレベルだけ独立していればよいケース
records = [3, 1, 2]
work = records.copy()  # または records[:] / copy.copy(records)
work.sort()
print("records:", records)  # 元は不変
print("work:", work)
実行結果
records: [3, 1, 2]
work: [1, 2, 3]

例2: リストのリストを安全に編集

Python
import copy

grid = [[0, 0], [0, 0]]
unsafe = grid.copy()              # シャローコピー。内側が共有される
safe = copy.deepcopy(grid)        # ディープコピー。完全に独立

unsafe[0][0] = 1
safe[1][1] = 2

print("grid:", grid)              # unsafe の変更が grid に伝播
print("unsafe:", unsafe)
print("safe:", safe)
実行結果
grid: [[1, 0], [0, 0]]
unsafe: [[1, 0], [0, 0]]
safe: [[0, 0], [0, 2]]

例3: リスト内の辞書を更新

Python
import copy

users = [{"id": 1, "tags": []}, {"id": 2, "tags": []}]
shallow = users.copy()
deep = copy.deepcopy(users)

shallow[0]["tags"].append("trial")  # 共有されているため元にも反映
deep[1]["tags"].append("pro")       # 完全に独立

print("users:", users)
print("shallow:", shallow)
print("deep:", deep)
実行結果
users: [{'id': 1, 'tags': ['trial']}, {'id': 2, 'tags': []}]
shallow: [{'id': 1, 'tags': ['trial']}, {'id': 2, 'tags': []}]
deep: [{'id': 1, 'tags': []}, {'id': 2, 'tags': ['pro']}]

テストでの検証ポイント

コピーの正しさは、同値性==と同一性isの両方を確認します。

特にネスト内のオブジェクトが共有されていないかをisで確かめると安心です。

コード例(簡易アサーション)

Python
import copy

a = [{"k": [1]}]
b = a.copy()               # シャロー
c = copy.deepcopy(a)       # ディープ

# 最上位リストは常に別物であるべき
assert a is not b and a is not c

# シャローコピーではネスト要素が共有される
assert a[0] is b[0]
assert a[0]["k"] is b[0]["k"]

# ディープコピーではネスト要素も別物
assert a[0] is not c[0]
assert a[0]["k"] is not c[0]["k"]

# 変更の独立性を確認
b[0]["k"].append(2)
assert a[0]["k"] == [1, 2]  # 共有されているため a にも影響

c[0]["k"].append(3)
assert a[0]["k"] == [1, 2]  # ディープコピーは影響しない

print("All tests passed.")
実行結果
All tests passed.

早見表(違いのまとめ)

方法最上位は新規かネストは新規か速度の目安典型用途
代入(=)いいえいいえ最速単に別名で参照したいだけ
list.copy / スライス / copy.copyはいいいえ速い並べ替えや追加削除など上位のみ編集
copy.deepcopyはいはい遅いネストを含む安全な編集、テスト用の独立データ

まとめ

リストのコピーでハマる本質は、代入が参照共有であり、シャローコピーではネスト内の要素が共有される点にあります。

上位だけ独立させたいならlist.copy、ネストを含めて完全に独立させたいならcopy.deepcopyを選びます。

性能面のコストを考慮しつつ、テストではis==の両面から独立性を確認してください。

意図がコードに表れる明示的なコピーを徹底することで、「もうハマらない」堅牢なPythonコードを書けるようになります。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

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

URLをコピーしました!