閉じる

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

リストをコピーしたのに中身が思わぬところで書き変わってしまう、そんな経験はありませんか。

原因の多くはシャローコピーとディープコピーの違いにあります。

本稿ではlist.copycopy.deepcopyの動作と使い分けを、中級者に進むための土台として丁寧に解説します。

Pythonのリストコピーの結論

list.copyはシャローコピー

list.copyはトップレベルのリストだけを新しく作り、内側の要素は同じオブジェクトを参照します

したがって、ネストしたリストや辞書が含まれる場合は、子要素の変更が元のリストに波及します。

Python
# シャローコピーの基本例
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は再帰的に全階層を複製し、子要素も別オブジェクトになります

ネストしたミュータブル要素が変更されても元の構造に影響しません。

Python
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演算子で確認できます。

Python
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)はすべてシャローコピーで、トップレベルだけが新しくなります

Python
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()と同じシャローコピーです

クラスによっては動作のフックが異なる場合がありますが、組み込みリストでは等価と考えて差し支えありません。

ネストしたリストの罠(子要素の変更が波及)

シャローコピー後に子リストを書き換えると、参照共有のため元のリストも変化します。

Python
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箇所の更新が全行に波及します。

Python
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

解決策は各行を個別に新規作成することです。

Python
# 正しい初期化
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

シャローコピーで十分なケース(イミュータブル要素)

リストがイミュータブル要素のみ(例:int, float, str, tuple)なら、シャローコピーで問題になりません。

イミュータブルは就地変更ができないため、参照を共有しても影響しないからです。

Python
names = ["Alice", "Bob", "Carol"]
names2 = names.copy()

names2[0] = "ALICE"   # 要素の差し替えはトップレベルの変更
print("元:", names)
print("複製:", names2)
実行結果
元: ["Alice", "Bob", "Carol"]
複製: ["ALICE", "Bob", "Carol"]

要素の差し替えはトップレベルの変更なので、シャローコピーで十分です。

ディープコピー(copy.deepcopy)の基礎

deepcopyの動作(再帰的に複製)

全階層のミュータブルを再帰的に新規オブジェクトで複製します。

ネストした辞書や集合を含む構造でも独立に扱えます。

Python
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の注意点(速度とメモリ)

ディープコピーは重いです。

構造が大きくなるほど時間とメモリを消費します。

必要がなければ避け、代替として必要な部分だけの手動コピーイミュータブル化を検討してください。

簡易ベンチマークの例を示します(値は実行環境で変わります)。

Python
# 注意: 実行環境により計測結果は大きく異なります
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倍

概ねディープコピーは桁違いに遅いことを覚えておくと設計で迷いません。

実践レシピとチェックリスト

バグ再現→原因→修正

実務で遭遇しやすいバグを通して、原因と修正を確認します。

Python
# スコア表を3行作るつもりで…(実は同じ行を3回参照している)
scores = [[0] * 3] * 3
scores[0][1] = 10

print("scores:", scores)
実行結果
scores: [[0, 10, 0], [0, 10, 0], [0, 10, 0]]

*による繰り返しは参照を複製するだけで、各行が同一オブジェクトになっていました。

そのため1行の更新がすべてに波及しました。

修正するには、各行を独立に生成する、またはネストをディープコピーします。

Python
# 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]]

正しい使い分けのフローチャート

以下の順に判断すると迷いません。

  1. 構造はネストしているか → いいえ → list.copy
  2. はい → 子要素にミュータブルがあるか → いいえ → list.copy
  3. はい → 複製後に子要素を就地更新するか → いいえ → list.copy
  4. はい → データ量は大きいか → はい → 一部だけ手動コピーや設計見直しを検討
  5. それでも全体の独立性が必要 → 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のエイリアス子要素の参照共有は初心者がつまずきやすい落とし穴です。

パフォーマンス面ではシャローが圧倒的に有利なので、必要最小限のコピーを心がけることがいいでしょう。

Python 実践TIPS - コーディング効率化・Pythonic
この記事を書いた人
エーテリア編集部
エーテリア編集部

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

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

URLをコピーしました!