リストをコピーしたのに中身が思わぬところで書き変わってしまう、そんな経験はありませんか。
原因の多くはシャローコピーとディープコピーの違いにあります。
本稿ではlist.copyとcopy.deepcopyの動作と使い分けを、中級者に進むための土台として丁寧に解説します。
Pythonのリストコピーの結論
list.copyはシャローコピー
list.copy
はトップレベルのリストだけを新しく作り、内側の要素は同じオブジェクトを参照します。
したがって、ネストしたリストや辞書が含まれる場合は、子要素の変更が元のリストに波及します。
# シャローコピーの基本例
nums = [[1, 2], [3, 4]]
shallow = nums.copy() # または nums[:], list(nums)
shallow[0].append(99) # 子リストを変更
print("元:", nums)
print("複製:", shallow)
print("子要素は同一参照か:", nums[0] is shallow[0])
元: [[1, 2, 99], [3, 4]]
複製: [[1, 2, 99], [3, 4]]
子要素は同一参照か: True
copy.deepcopyはディープコピー
copy.deepcopy
は再帰的に全階層を複製し、子要素も別オブジェクトになります。
ネストしたミュータブル要素が変更されても元の構造に影響しません。
import copy
nums = [[1, 2], [3, 4]]
deep = copy.deepcopy(nums)
deep[0].append(88)
print("元:", nums)
print("複製:", deep)
print("子要素は同一参照か:", nums[0] is deep[0])
元: [[1, 2], [3, 4]]
複製: [[1, 2, 88], [3, 4]]
子要素は同一参照か: False
使い分けの基本指針
トップレベルだけ別になればよいならシャロー、ネストしたミュータブルを独立させたいならディープが原則です。
速度とメモリの観点ではシャローコピーが軽量です。
下表は最短の判断基準です。
データ構造 | 要素の性質 | 推奨コピー |
---|---|---|
フラットなリスト | イミュータブルのみ(int, str, tupleなど) | list.copy |
ネストなし | ミュータブルなし | list.copy |
ネストあり | ミュータブルを含む(list, dict, setなど) | copy.deepcopy |
複製後に子要素を更新する予定 | あり | copy.deepcopy |
パフォーマンス優先で読み取り専用 | あり | list.copy または参照共有 |
シャローコピー(list.copy)の基礎
シャローコピーとは?(参照は共有)
シャローコピーは外側のコンテナだけ新しくなる一方で、内側のオブジェクト参照は共有する方式です。
したがって、内側のオブジェクトを就地更新すると双方に影響します。
これはis
演算子で確認できます。
data = [[0], [1]]
sh = data.copy()
print("外側は別物か:", sh is data)
print("内側0番は同一参照か:", sh[0] is data[0])
print("内側1番は同一参照か:", sh[1] is data[1])
外側は別物か: True
内側0番は同一参照か: True
内側1番は同一参照か: True
list.copy/スライス(:)/list()は同じ効果
lst.copy()
、lst[:]
、list(lst)
はすべてシャローコピーで、トップレベルだけが新しくなります。
import copy
lst = [[1], [2]]
a = lst.copy() # メソッド
b = lst[:] # スライス
c = list(lst) # コンストラクタ
d = copy.copy(lst) # 標準ライブラリの浅いコピー
print("a is lst:", a is lst)
print("b is lst:", b is lst)
print("c is lst:", c is lst)
print("d is lst:", d is lst)
print("a[0] is lst[0]:", a[0] is lst[0])
print("b[0] is lst[0]:", b[0] is lst[0])
print("c[0] is lst[0]:", c[0] is lst[0])
print("d[0] is lst[0]:", d[0] is lst[0])
a is lst: False
b is lst: False
c is lst: False
d is lst: False
a[0] is lst[0]: True
b[0] is lst[0]: True
c[0] is lst[0]: True
d[0] is lst[0]: True
copy.copyとの関係
copy.copy(x)
はリストに対してx.copy()
と同じシャローコピーです。
クラスによっては動作のフックが異なる場合がありますが、組み込みリストでは等価と考えて差し支えありません。
ネストしたリストの罠(子要素の変更が波及)
シャローコピー後に子リストを書き換えると、参照共有のため元のリストも変化します。
a = [[1, 2], [3, 4]]
b = a.copy()
b[1].remove(4) # 子要素の就地変更
print("a:", a)
print("b:", b)
a: [[1, 2], [3]]
b: [[1, 2], [3]]
[x]*nの複製はエイリアス地獄
[[0] * 3] * 3
のような生成は、内側のリスト参照を3回繰り返すだけです。
1箇所の更新が全行に波及します。
grid = [[0] * 3] * 3
grid[0][0] = 1 # 1箇所だけ変えたつもりが…
print("grid:", grid)
print("行0と行1は同一オブジェクトか:", grid[0] is grid[1])
grid: [[1, 0, 0], [1, 0, 0], [1, 0, 0]]
行0と行1は同一オブジェクトか: True
解決策は各行を個別に新規作成することです。
# 正しい初期化
grid = [[0 for _ in range(3)] for _ in range(3)]
grid[0][0] = 1
print("grid:", grid)
print("行0と行1は同一オブジェクトか:", grid[0] is grid[1])
grid: [[1, 0, 0], [0, 0, 0], [0, 0, 0]]
行0と行1は同一オブジェクトか: False
- 関連記事:文字列結合と繰り返しの基本(+と*の使い方)
- 関連記事:for文を置き換えるリスト内包表記の使い方とコツ
- binary-arithmetic-operations — Python 3.13.7 ドキュメント
シャローコピーで十分なケース(イミュータブル要素)
リストがイミュータブル要素のみ(例:int, float, str, tuple)なら、シャローコピーで問題になりません。
イミュータブルは就地変更ができないため、参照を共有しても影響しないからです。
names = ["Alice", "Bob", "Carol"]
names2 = names.copy()
names2[0] = "ALICE" # 要素の差し替えはトップレベルの変更
print("元:", names)
print("複製:", names2)
元: ["Alice", "Bob", "Carol"]
複製: ["ALICE", "Bob", "Carol"]
要素の差し替えはトップレベルの変更なので、シャローコピーで十分です。
ディープコピー(copy.deepcopy)の基礎
deepcopyの動作(再帰的に複製)
全階層のミュータブルを再帰的に新規オブジェクトで複製します。
ネストした辞書や集合を含む構造でも独立に扱えます。
import copy
state = {"users": [{"id": 1, "tags": {"vip"}}, {"id": 2, "tags": set()}]}
cloned = copy.deepcopy(state)
cloned["users"][0]["tags"].add("new")
print("元:", state)
print("複製:", cloned)
print("タグ集合は同一参照か:", state["users"][0]["tags"] is cloned["users"][0]["tags"])
元: {'users': [{'id': 1, 'tags': {'vip'}}, {'id': 2, 'tags': set()}]}
複製: {'users': [{'id': 1, 'tags': {'vip', 'new'}}, {'id': 2, 'tags': set()}]}
タグ集合は同一参照か: False
deepcopyが必要な場面(ネスト+ミュータブル)
以下のようにネストされたミュータブルを独立して更新したい場面ではcopy.deepcopy
を使います。
- リストのリスト、リストの辞書、辞書のリストなどの複合データ構造
- 複製後に子要素を就地更新する処理がある
- 元データを厳密に保全したいテストやバックアップ用途
deepcopyの注意点(速度とメモリ)
ディープコピーは重いです。
構造が大きくなるほど時間とメモリを消費します。
必要がなければ避け、代替として必要な部分だけの手動コピーやイミュータブル化を検討してください。
簡易ベンチマークの例を示します(値は実行環境で変わります)。
# 注意: 実行環境により計測結果は大きく異なります
import copy, time
def bench(n_rows: int, row_len: int = 50):
base = [[0] * row_len for _ in range(n_rows)]
t0 = time.perf_counter()
shallow = base.copy()
t1 = time.perf_counter()
deep = copy.deepcopy(base)
t2 = time.perf_counter()
return (t1 - t0), (t2 - t1)
for n in (1_000, 5_000):
s, d = bench(n)
print(f"行数={n}, シャロー={s:.6f}s, ディープ={d:.6f}s, 比={d/max(s, 1e-9):.1f}倍")
行数=1000, シャロー=0.0001s, ディープ=0.0045s, 比=45.0倍
行数=5000, シャロー=0.0003s, ディープ=0.0230s, 比=76.7倍
概ねディープコピーは桁違いに遅いことを覚えておくと設計で迷いません。
実践レシピとチェックリスト
バグ再現→原因→修正
実務で遭遇しやすいバグを通して、原因と修正を確認します。
# スコア表を3行作るつもりで…(実は同じ行を3回参照している)
scores = [[0] * 3] * 3
scores[0][1] = 10
print("scores:", scores)
scores: [[0, 10, 0], [0, 10, 0], [0, 10, 0]]
*
による繰り返しは参照を複製するだけで、各行が同一オブジェクトになっていました。
そのため1行の更新がすべてに波及しました。
修正するには、各行を独立に生成する、またはネストをディープコピーします。
# 1) 正しい初期化
scores = [[0 for _ in range(3)] for _ in range(3)]
scores[0][1] = 10
print("独立行:", scores)
# 2) 既存の雛形を増やす場合はdeepcopy
import copy
row = [0, 0, 0]
scores2 = [copy.deepcopy(row) for _ in range(3)]
scores2[0][1] = 10
print("deepcopy版:", scores2)
独立行: [[0, 10, 0], [0, 0, 0], [0, 0, 0]]
deepcopy版: [[0, 10, 0], [0, 0, 0], [0, 0, 0]]
正しい使い分けのフローチャート
以下の順に判断すると迷いません。
- 構造はネストしているか → いいえ → list.copy
- はい → 子要素にミュータブルがあるか → いいえ → list.copy
- はい → 複製後に子要素を就地更新するか → いいえ → list.copy
- はい → データ量は大きいか → はい → 一部だけ手動コピーや設計見直しを検討
- それでも全体の独立性が必要 → copy.deepcopy
テキスト版の流れ: 開始 → ネストあり? → なし: list.copy → あり: 子がミュータブル? → なし: list.copy → あり: 子を更新? → なし: list.copy → あり: データ大? → 大: 局所コピー/設計変更 → 小〜中: deepcopy
パフォーマンスの目安(シャロー vs ディープ)
全体感を掴むための目安を示します。
値はあくまで一例です。
観点 | シャローコピー(list.copy) | ディープコピー(copy.deepcopy) |
---|---|---|
時間計算量 | O(n) 参照配列の複写 | O(総要素数) 再帰的に全複製 |
メモリ | 低い | 高い |
典型速度感 | 非常に速い | 数十倍〜数百倍遅いことも |
安全性(ネスト更新) | 影響が波及 | 影響しない |
簡易計測コードとサンプル出力は前節を参照してください。
まずはシャローで設計し、必要な箇所だけディープにするのが実務的です。
まとめ
list.copyはトップレベルのみ複製するシャローコピー、copy.deepcopyは全階層を複製するディープコピーです。
ネストしたミュータブルを就地更新するならdeepcopy、そうでなければlist.copyが基本方針です。
特に[x]*nのエイリアスと子要素の参照共有は初心者がつまずきやすい落とし穴です。
パフォーマンス面ではシャローが圧倒的に有利なので、必要最小限のコピーを心がけることがいいでしょう。