「コピーしたのに元のリストまで変わってしまった」そんな経験は、Pythonのリストが参照を共有する性質や、シャローコピーとディープコピーの違いを理解すれば防げます。
本記事では、ハマりどころを段階的に解説し、list.copy
とcopy.deepcopy
の正しい使い分けを実例付きで説明します。
Pythonのリストのコピーでハマる理由
Pythonのリストはミュータブル(変更可能)なオブジェクトです。
コピー方法を誤ると、思わぬ箇所で値が連動してしまいます。
ここでは、よくある勘違いの根本原因を整理します。
代入は参照共有でコピーではない
Pythonでb = a
と書くと、新しいリストが作られるわけではありません。
変数a
とb
が同じリストオブジェクトを参照します。
# 代入はコピーではなく、同じオブジェクトを指す参照共有です
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
ミュータブルとイミュータブルの違い
リストや辞書はミュータブル、整数や文字列、タプルはイミュータブルです。
ミュータブルは「中身をその場で変更」でき、イミュータブルは「新しいオブジェクトを作って置き換え」ます。
この違いが、コピー時の挙動に影響します。
# イミュータブル(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]]
ネストで変更が伝播する
ネストされたリストや辞書では、上位の容器だけをコピーしても、内側の要素が共有され続けるため変更が伝播します。
# 上位だけコピーしても、内側は共有される典型例
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()
は最上位のリストのみを複製します。
内側のオブジェクトは同じ参照を保持します。
# 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()
と同じくシャローコピーです。
可読性や一貫性の観点で、チーム規約に合わせて選ぶとよいです。
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
ネスト要素は共有される
ネスト構造では、シャローコピーでは不十分です。
内側の参照が共有されるため、内側の変更が元のリストに影響します。
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
は、可能な限り全階層で新しいオブジェクトを作ります。
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
は循環参照を検出して安全に処理しますが、コストは高くなりがちです。
大きく深い構造では、必要な部分だけを個別にコピーする戦略も検討します。
コード例(循環参照)
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
コード例(負荷の目安)
# 注意: 実行環境で時間は変わります。相対比較の目安です。
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
は属性をたどって複製しますが、外部リソースやキャッシュなどは再利用したい場合があります。
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: 単純な複製と並べ替え
# トップレベルだけ独立していればよいケース
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: リストのリストを安全に編集
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: リスト内の辞書を更新
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
で確かめると安心です。
コード例(簡易アサーション)
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コードを書けるようになります。