copy(浅いコピー)は「外側の入れ物だけ」を新しく作り、deepcopy(深いコピー)は「中に入っているオブジェクトまですべて」新しく作ります。 そのため、リストの中にリストがあるようなネストしたデータでは、浅いコピーだと内側が元データと共有されたままになり、深いコピーだと完全に独立します。

この記事では、代入・スライス・list.copy()・copy.copy・copy.deepcopy の5通りを同じネストリストで実行し、id() と出力を並べて比べます。 さらに、2次元リストを掛け算で作ったときに行が連動する例、dict や自作クラス、循環参照、__deepcopy__、Python 3.13 で追加された copy.replace、timeit で測った速度までを扱います。実行結果はすべて Python 3.14.7 と Python 3.11.15 で実際に動かしたものです。

copyとdeepcopyの早見表

同じリスト a = [1, [2, 3]] を5通りの方法で b にし、b.append(99)(外側の変更)と b[1].append(4)(内側の変更)を行った結果です。

書き方 種類 id(a) == id(b) id(a[1]) == id(b[1]) 変更後の a 変更後の b
b = a 代入(コピーではない) True True [1, [2, 3, 4], 99] [1, [2, 3, 4], 99]
b = a[:] 浅いコピー False True [1, [2, 3, 4]] [1, [2, 3, 4], 99]
b = a.copy() 浅いコピー False True [1, [2, 3, 4]] [1, [2, 3, 4], 99]
b = copy.copy(a) 浅いコピー False True [1, [2, 3, 4]] [1, [2, 3, 4], 99]
b = copy.deepcopy(a) 深いコピー False False [1, [2, 3]] [1, [2, 3, 4], 99]

読み取り方は次の3点です。

  • 代入(b = a)は同じリストに別の名前を付けるだけなので、b への変更はすべて a にも現れます。
  • 浅いコピー(スライス・.copy()・copy.copy)は外側だけが別物です。append(99) は a に影響しませんが、内側のリスト [2, 3] は共有されているので 4 が a にも入ります。
  • 深いコピー(copy.deepcopy)は内側も別物になり、a は元の [1, [2, 3]] のまま残ります。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Python copyとdeepcopyの違い|浅い/深いコピー早見表」

5通りのコピーを同じコードで比べる

検証コードと実行結果

早見表のもとになったコードです。 そのまま保存して python3 compare.py で動きます。

import copy

ways = {
    "b = a":            lambda a: a,
    "b = a[:]":         lambda a: a[:],
    "b = a.copy()":     lambda a: a.copy(),
    "b = copy.copy(a)": lambda a: copy.copy(a),
    "b = copy.deepcopy(a)": lambda a: copy.deepcopy(a),
}
for label, make in ways.items():
    a = [1, [2, 3]]
    b = make(a)
    print(label)
    print("  id(a)   ==id(b)   :", id(a) == id(b), f"({id(a)} / {id(b)})")
    print("  id(a[1])==id(b[1]):", id(a[1]) == id(b[1]), f"({id(a[1])} / {id(b[1])})")
    b.append(99)       # 外側を変更
    b[1].append(4)     # 内側を変更
    print("  a =", a)
    print("  b =", b)

Python 3.14.7 での実行結果です(id() の数値は実行のたびに変わります。True / False の並びは 3.11.15 でも同じでした)。

b = a
  id(a)   ==id(b)   : True (140567635320768 / 140567635320768)
  id(a[1])==id(b[1]): True (140567628534848 / 140567628534848)
  a = [1, [2, 3, 4], 99]
  b = [1, [2, 3, 4], 99]
b = a[:]
  id(a)   ==id(b)   : False (140567629098688 / 140567629100544)
  id(a[1])==id(b[1]): True (140567629101696 / 140567629101696)
  a = [1, [2, 3, 4]]
  b = [1, [2, 3, 4], 99]
b = a.copy()
  id(a)   ==id(b)   : False (140567628534848 / 140567629098688)
  id(a[1])==id(b[1]): True (140567635320768 / 140567635320768)
  a = [1, [2, 3, 4]]
  b = [1, [2, 3, 4], 99]
b = copy.copy(a)
  id(a)   ==id(b)   : False (140567629101696 / 140567628534848)
  id(a[1])==id(b[1]): True (140567629100544 / 140567629100544)
  a = [1, [2, 3, 4]]
  b = [1, [2, 3, 4], 99]
b = copy.deepcopy(a)
  id(a)   ==id(b)   : False (140567635320768 / 140567629101696)
  id(a[1])==id(b[1]): False (140567629098688 / 140567628545856)
  a = [1, [2, 3]]
  b = [1, [2, 3, 4], 99]

id() は、オブジェクトごとに割り振られる番号を返す組み込み関数です。2つの変数の id() が同じなら、それは同じオブジェクトを指しています。 a is b と書いても同じ判定ができます。

なぜそうなるか:変数は「値の入れ物」ではなく「名札」

Pythonの変数は、オブジェクトに付ける名札のようなものです。b = a は名札を1枚増やすだけで、リストそのものは増えません。

flowchart LR
    subgraph 代入
      a1[a] --> L1["リスト [1, ・]"]
      b1[b] --> L1
      L1 --> I1["内側 [2, 3]"]
    end
    subgraph 浅いコピー
      a2[a] --> L2["リスト [1, ・]"]
      b2[b] --> L3["新しいリスト [1, ・]"]
      L2 --> I2["内側 [2, 3]"]
      L3 --> I2
    end
    subgraph 深いコピー
      a3[a] --> L4["リスト [1, ・]"]
      b3[b] --> L5["新しいリスト [1, ・]"]
      L4 --> I3["内側 [2, 3]"]
      L5 --> I4["新しい内側 [2, 3]"]
    end

浅いコピーは「外側のリストを新しく作り、中身は元と同じオブジェクトへの参照を並べる」動きです。中身が数値や文字列のように変更できない(イミュータブルな)値だけなら、共有されていても困りません。中身がリストや辞書のように変更できる(ミュータブルな)値のときに、内側の変更が両方に現れます。

浅いコピーの書き方はどれを選ぶか

リストの浅いコピーは書き方が複数ありますが、結果は同じです。

書き方 対象 特徴
a.copy() list・dict・set など 意図がいちばん伝わりやすい
a[:] list・tuple・str などスライスできる型 以前からある書き方
list(a) あらゆるイテラブル 型を list にそろえたいときに便利
copy.copy(a) ほぼすべてのオブジェクト 型がわからないとき、自作クラスにも使える

リストだとわかっているなら a.copy()、型を問わず使いたいなら copy.copy(a) が読みやすい選び方です。

1次元のリストは浅いコピーで十分

中身が数値や文字列だけのリストは、浅いコピーで完全に独立します。 数値や文字列は変更できない値なので、b[0] = 100 は「b の0番目の名札を別の値に付け替える」操作になり、a には影響しません。

a = [1, 2, 3]
b = a.copy()
b[0] = 100
print(a)   # [1, 2, 3]
print(b)   # [100, 2, 3]

ネストしたリストでも、要素の「付け替え」は独立、要素の「中身の書き換え」は共有という違いがあります。

a = [1, [2, 3]]
b = a.copy()
b[0] = 100        # 付け替え → a は変わらない
print(a, b)       # [1, [2, 3]] [100, [2, 3]]
b[1][0] = 200     # 内側リストの中身を書き換え → a にも現れる
print(a, b)       # [1, [200, 3]] [100, [200, 3]]

2次元リストで行が連動するとき

掛け算で作ると全行が同じリストになる

[[0] * 3] * 3 で作った2次元リストは、3つの行がすべて同じリストを指します。 1か所を書き換えると全行が変わります。

g = [[0] * 3] * 3
g[0][0] = 1
print(g)                # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]
print(g[0] is g[1])     # True

リストの * は「同じ要素への参照を繰り返し並べる」演算です。内側の [0] * 3 は数値の繰り返しなので問題になりませんが、外側の * 3 は「同じ行リストへの参照を3つ並べる」動きになります。

行ごとに新しいリストを作るには、リスト内包表記を使います。

g = [[0] * 3 for _ in range(3)]
g[0][0] = 1
print(g)                # [[1, 0, 0], [0, 0, 0], [0, 0, 0]]
print(g[0] is g[1])     # False

2次元リストをコピーする3つの方法

2次元リストに .copy() を使うと、行リストは共有されたままです。

g = [[1, 2], [3, 4]]
h = g.copy()
h[0][0] = 9
print(g)        # [[9, 2], [3, 4]]  元も変わる
方法 書き方 向いている場面
行ごとに浅いコピー h = [row.copy() for row in g] ちょうど2階層のリスト。速い
スライスで行ごとに h = [row[:] for row in g] 上と同じ結果
深いコピー h = copy.deepcopy(g) 階層の深さがわからない、3階層以上
import copy

g = [[1, 2], [3, 4]]
h = [row.copy() for row in g]
h[0][0] = 9
print(g, h)     # [[1, 2], [3, 4]] [[9, 2], [3, 4]]

k = copy.deepcopy(g)
k[1][1] = 0
print(g, k)     # [[1, 2], [3, 4]] [[1, 2], [3, 0]]

dict・set・自作クラスのコピー

dict のコピー

dict も考え方は同じで、d.copy()・dict(d)・{**d}・d | {} はすべて浅いコピーです。

import copy

d = {"name": "A", "tags": ["x"], "opt": {"n": 1}}
e = d.copy()
e["name"] = "B"          # 値の付け替え → d は変わらない
e["tags"].append("y")    # 内側リストの書き換え → d にも現れる
print(d)   # {'name': 'A', 'tags': ['x', 'y'], 'opt': {'n': 1}}
print(e)   # {'name': 'B', 'tags': ['x', 'y'], 'opt': {'n': 1}}

d = {"name": "A", "tags": ["x"]}
f = copy.deepcopy(d)
f["tags"].append("y")
print(d, f)   # {'name': 'A', 'tags': ['x']} {'name': 'A', 'tags': ['x', 'y']}
書き方 種類 e["tags"] is d["tags"]
e = d 代入 True
e = d.copy() 浅いコピー True
e = dict(d) 浅いコピー True
e = {**d} 浅いコピー True
e = d | {} 浅いコピー(3.9以降) True
e = copy.deepcopy(d) 深いコピー False

set の s.copy() も浅いコピーです。set の要素はハッシュ可能な(基本的に変更できない)値なので、浅いコピーで独立して使えます。

自作クラスのインスタンス

自作クラスのインスタンスには .copy() メソッドがないので、copy.copy と copy.deepcopy を使います。

import copy

class Task:
    def __init__(self, title, tags):
        self.title = title
        self.tags = tags
    def __repr__(self):
        return f"Task({self.title!r}, {self.tags!r})"

t1 = Task("掃除", ["家"])
t2 = copy.copy(t1)
t2.title = "洗濯"            # 属性の付け替え → t1 は変わらない
t2.tags.append("急ぎ")       # 属性のリストを書き換え → t1 にも現れる
print(t1, t2)                # Task('掃除', ['家', '急ぎ']) Task('洗濯', ['家', '急ぎ'])
print(t2.tags is t1.tags)    # True

t1 = Task("掃除", ["家"])
t3 = copy.deepcopy(t1)
t3.tags.append("急ぎ")
print(t1, t3)                # Task('掃除', ['家']) Task('掃除', ['家', '急ぎ'])

copy.copy はインスタンスを新しく作り、属性(__dict__ の中身)を浅くコピーします。属性にリストや辞書を持つクラスでは、内側まで独立させたいときに copy.deepcopy を選びます。dataclass でも結果は同じでした。

イミュータブルな値はコピーしても同じオブジェクト

数値・文字列・タプルなど変更できない値は、コピーしても同じオブジェクトがそのまま返ります。 変更できないので、共有しても問題が起きないためです。

式 結果 説明
copy.copy((1, 2, 3)) is t True タプルはそのまま返る
copy.deepcopy((1, 2, 3)) is t True 中身も変更できない値だけなら同じ
copy.deepcopy((1, [2])) is t False 中に変更できる値があれば新しく作る
copy.deepcopy("abc") is s True 文字列はそのまま返る
copy.copy(frozenset([1])) is fs True frozenset もそのまま返る

(t・s・fs はそれぞれコピー元の変数です。)タプルの中にリストが入っている (1, [2]) は、タプル自体は変更できなくても中のリストは変更できます。この場合 copy.copy では中のリストが共有され、copy.deepcopy では中のリストも新しく作られます。

循環参照と共有参照:deepcopyのmemo

自分自身を含むリストもコピーできる

循環参照(自分自身を要素に持つデータ)があっても、copy.deepcopy は止まらずにコピーを完了します。

import copy

a = [1, 2]
a.append(a)             # 自分自身を要素に追加
print(a)                # [1, 2, [...]]

b = copy.deepcopy(a)
print(b)                # [1, 2, [...]]
print(b[2] is b)        # True   コピー側は「コピー自身」を指す
print(b[2] is a)        # False  元のリストは指していない

c = a.copy()
print(c[2] is a)        # True   浅いコピーは元のリストを指したまま

親子が互いを参照するツリーでも、構造を保ったままコピーされます。

import copy

class Node:
    def __init__(self, name):
        self.name = name
        self.parent = None
        self.children = []

parent = Node("親")
child = Node("子")
child.parent = parent
parent.children.append(child)

p2 = copy.deepcopy(parent)
print(p2 is parent)                      # False
print(p2.children[0].parent is p2)       # True   コピー側の中でつながっている
print(p2.children[0] is child)           # False

しくみ:コピー済みのオブジェクトを memo に記録する

copy.deepcopy は、「元オブジェクトの id → コピーしたオブジェクト」の対応表(memo 辞書)を持ちながら再帰的にコピーします。すでに memo にあるオブジェクトに再び出会ったら、新しく作らずに記録済みのコピーを使います。公式ドキュメント(copy モジュール、Python 3.14.8 のページ)でも、memo によって再帰ループと過剰なコピーを避けると説明されています。

このしくみのおかげで、同じオブジェクトを2か所から参照している関係(共有参照)もコピー後に保たれます。

import copy, json

shared = [0]
a = [shared, shared]            # 同じリストを2回入れる

b = copy.deepcopy(a)
print(b[0] is b[1])             # True   共有の関係が保たれる
print(b[0] is shared)           # False  元とは別物

j = json.loads(json.dumps(a))
print(j[0] is j[1])             # False  JSON経由では別々のリストになる

JSON に変換して戻す方法でも深いコピーに近いことができますが、共有の関係は保たれず、循環参照は扱えません。また、タプルがリストになり、日時や自作クラスは変換できないため、JSON で表せる単純なデータに限った方法です。

階層がとても深いデータ

deepcopy は再帰でたどるため、ネストが極端に深いデータでは上限に達します。リストを2,000段入れ子にしたデータで試すと、RecursionError: maximum recursion depth exceeded になりました(3.14.7・3.11.15とも)。通常のデータでこの深さになることはまれですが、連結リストのような構造を自作する場合は、後述の __deepcopy__ でループを使ったコピーを書くと対応できます。

コピーの動きを変える:__copy__ と __deepcopy__

自作クラスに __copy__ と __deepcopy__ を定義すると、copy.copy と copy.deepcopy が呼ばれたときの動きを決められます。 「大きなキャッシュは共有したまま、ほかの属性だけ深くコピーしたい」といった場面で使います。

import copy

class Config:
    def __init__(self, name, items, cache=None):
        self.name = name
        self.items = items
        self.cache = cache if cache is not None else {}

    def __copy__(self):
        print("  __copy__ called")
        return Config(self.name, self.items, self.cache)

    def __deepcopy__(self, memo):
        print("  __deepcopy__ called")
        new = Config(self.name, copy.deepcopy(self.items, memo), self.cache)  # cache は共有
        memo[id(self)] = new
        return new

c1 = Config("main", [[1, 2]], {"big": "data"})
c2 = copy.copy(c1)
c3 = copy.deepcopy(c1)
print(c3.items is c1.items, c3.items[0] is c1.items[0], c3.cache is c1.cache)
  __copy__ called
  __deepcopy__ called
False False True

items は内側まで別物になり、cache は共有されたままです。書くときのポイントは2つです。

  • __deepcopy__ の引数 memo を、内側の copy.deepcopy(…, memo) にそのまま渡します。 こうすると循環参照や共有参照の扱いが引き継がれます。
  • 自分自身を memo[id(self)] = new で登録します。 自分を参照する属性があっても、同じコピーが使われます。

deepcopy できないオブジェクト

ロック・開いているファイル・ジェネレータなど、OSの資源や実行状態を持つオブジェクトは deepcopy できません。 属性にこれらを持つクラスをコピーすると、同じエラーになります。

対象 Python 3.14.7 での結果
threading.Lock() TypeError: cannot pickle '_thread.lock' object
open(...) で開いたファイル TypeError: cannot pickle 'TextIOWrapper' instances
ジェネレータ TypeError: cannot pickle 'generator' object
モジュール(math など) TypeError: cannot pickle 'module' object
関数(len、lambda)・クラス そのまま同じオブジェクトが返る

3.11.15 ではファイルのメッセージが cannot pickle '_io.TextIOWrapper' object でした。このエラーが出たら、__deepcopy__ を定義してその属性だけ共有する(または作り直す)のが定番の対処です。

Python 3.13 の copy.replace:一部だけ変えたコピーを作る

copy.replace(obj, **changes) は、指定したフィールドだけを差し替えた新しいオブジェクトを返す関数で、Python 3.13 で追加されました。 公式ドキュメントによると、対応するのは namedtuple、dataclass、そして __replace__() メソッドを定義したクラスです。変更できない(frozen な)オブジェクトの「一部だけ違う版」を作るときに便利です。

import copy
import datetime
from collections import namedtuple
from dataclasses import dataclass

@dataclass(frozen=True)
class User:
    name: str
    role: str = "member"
    tags: tuple = ()

u = User("sato")
print(copy.replace(u, role="admin"), u)

P = namedtuple("P", "x y")
print(copy.replace(P(1, 2), x=10))
print(copy.replace(datetime.date(2026, 10, 3), day=1))

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    def __replace__(self, **changes):
        return Point(changes.get("x", self.x), changes.get("y", self.y))
    def __repr__(self):
        return f"Point({self.x}, {self.y})"

print(copy.replace(Point(1, 2), y=5))

Python 3.14.7 での実行結果です。

User(name='sato', role='admin', tags=()) User(name='sato', role='member', tags=())
P(x=10, y=2)
2026-10-01
Point(1, 5)

つまずきやすい点も実行して確かめました。

操作 結果
copy.replace([1, 2], x=1) TypeError: replace() does not support list objects
copy.replace(u, age=3)(ないフィールド) TypeError: User.__init__() got an unexpected keyword argument 'age'
Python 3.11.15 で copy.replace AttributeError: module 'copy' has no attribute 'replace'
dataclass の list フィールドを変えずに replace 新旧で同じリストを共有(浅いコピーと同じ)

copy.replace は差し替えなかったフィールドを浅く引き継ぎます。 リストのフィールドを持つ dataclass では、b2 = copy.replace(b1) のあと b2.items is b1.items が True でした。3.12 以前では、dataclass なら dataclasses.replace(u, role="admin")、namedtuple なら p._replace(x=10) で同じことができます(どちらも 3.11.15 で動作を確かめました)。

deepcopyの速度をtimeitで実測

測定コード

deepcopy は便利な反面、すべての要素をたどるので時間がかかります。 1,000要素のデータで実際に測りました。

import copy, timeit, json, pickle

flat = list(range(1000))
nested = [[i, i + 1, i + 2] for i in range(1000)]
records = [{"id": i, "name": f"user{i}", "tags": ["a", "b"]} for i in range(1000)]

def bench(label, fn, n=200):
    t = min(timeit.repeat(fn, number=n, repeat=5)) / n * 1e6
    print(f"{label:38} {t:10.1f} µs")

bench("flat.copy()", lambda: flat.copy())
bench("copy.deepcopy(flat)", lambda: copy.deepcopy(flat))
bench("nested.copy()", lambda: nested.copy())
bench("[r.copy() for r in nested]", lambda: [r.copy() for r in nested])
bench("copy.deepcopy(nested)", lambda: copy.deepcopy(nested))
bench("[dict(r) for r in records]", lambda: [dict(r) for r in records])
bench("[{**r,'tags':r['tags'].copy()}...]",
      lambda: [{**r, "tags": r["tags"].copy()} for r in records])
bench("copy.deepcopy(records)", lambda: copy.deepcopy(records), 50)
bench("json.loads(json.dumps(records))", lambda: json.loads(json.dumps(records)), 50)
bench("pickle.loads(pickle.dumps(records))", lambda: pickle.loads(pickle.dumps(records)), 50)

測定結果

1回あたりの時間(マイクロ秒、5回のうち最小値)です。検証用のLinuxコンテナでの値なので、数値そのものより比率を目安にしてください。

対象 方法 Python 3.14.7 Python 3.11.15
整数1,000個のリスト flat.copy() 1.6 µs 1.6 µs
整数1,000個のリスト flat[:] 1.5 µs 1.7 µs
整数1,000個のリスト copy.copy(flat) 1.7 µs 1.8 µs
整数1,000個のリスト copy.deepcopy(flat) 63.3 µs 176.7 µs
3要素×1,000行のリスト nested.copy()(浅い) 2.3 µs 2.3 µs
3要素×1,000行のリスト [r.copy() for r in nested] 50.6 µs 43.1 µs
3要素×1,000行のリスト copy.deepcopy(nested) 652.6 µs 1302.6 µs
dict 1,000件のリスト [dict(r) for r in records](1階層) 71.9 µs 77.0 µs
dict 1,000件のリスト 内包表記で tags まで手動コピー 169.3 µs 186.4 µs
dict 1,000件のリスト copy.deepcopy(records) 1558.2 µs 2533.1 µs
dict 1,000件のリスト json.loads(json.dumps(records)) 886.4 µs 1131.7 µs
dict 1,000件のリスト pickle.loads(pickle.dumps(records)) 420.0 µs 461.9 µs

読み取れることは次のとおりです。

  • 浅いコピーは非常に速く、1,000要素でも約2マイクロ秒でした。 スライス・.copy()・copy.copy の差はほぼありません。
  • deepcopy は、同じ結果になる手書きのコピーと比べて、3.14.7 で約9〜13倍、3.11.15 で約14〜30倍の時間がかかりました(2次元リストでは 652.6 ÷ 50.6、dict のリストでは 1558.2 ÷ 169.3)。
  • 3.14.7 の deepcopy は 3.11.15 より約1.6〜2.8倍速い結果でした。 新しいPythonほど負担は小さくなっています。
  • 1,000件の dict でも deepcopy は約1.6ミリ秒です。1回だけのコピーなら体感できる差にはならず、ループの中で何千回も呼ぶときに効いてきます。

使い分けの判断基準

判断表

「中身に変更できる値があるか」と「コピー後に内側を書き換えるか」の2点で決まります。

データと使い方 選ぶもの 理由
元と同じものを別名で扱いたい 代入 b = a コピーは不要
数値・文字列だけのリストや dict 浅いコピー a.copy() 中身が変更できない値なので独立する
ネストがあるが、外側の追加・削除・並べ替えだけ行う 浅いコピー a.copy() 内側に触れないなら共有でも影響しない
ネストがあり、内側も書き換える 深いコピー copy.deepcopy(a) 内側まで独立させる
階層が決まっていて、速度も重視する 内包表記で手動コピー 測定では deepcopy の約9〜30分の1の時間
構造がわからないデータ(外部から受け取った設定など) 深いコピー copy.deepcopy(a) 階層を調べなくても確実に独立する
frozen な dataclass の一部だけ変える copy.replace(3.13以降) 新しいインスタンスを1行で作れる
コピーできない属性(ロックなど)を持つクラス __deepcopy__ を定義 その属性だけ共有・再作成する
flowchart TD
    A[コピーしたい] --> B{中身にリストやdictなど<br>変更できる値があるか}
    B -- ない --> C[浅いコピー a.copy]
    B -- ある --> D{コピー後に内側を書き換えるか}
    D -- 書き換えない --> C
    D -- 書き換える --> E{階層が決まっていて<br>何度も呼ぶ処理か}
    E -- はい --> F[内包表記で手動コピー]
    E -- いいえ --> G[copy.deepcopy]

よくある場面と直し方

関数に渡したリストを関数の中で書き換えると、呼び出し元のリストも変わります。 引数も代入と同じく「名札を付けるだけ」だからです。関数の先頭でコピーすると、呼び出し元に影響しない関数になります。

def add_tag(tags, tag):
    tags = tags.copy()      # 受け取ったリストをコピーしてから変更
    tags.append(tag)
    return tags

orig = ["a"]
print(add_tag(orig, "b"), orig)   # ['a', 'b'] ['a']

デフォルト引数にリストを書いたときも、同じリストが呼び出しのたびに使い回されます。

def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item(1), add_item(2))   # [1, 2] [1, 2]

items=None としておき、関数の中で if items is None: items = [] と書くと、呼び出しごとに新しいリストになります。

症状 原因 直し方
コピーしたはずなのに元のデータも変わる 代入、または浅いコピーで内側を書き換えた 内側も書き換えるなら copy.deepcopy
2次元リストの1か所を変えたら全行が変わる [[0] * 3] * 3 で同じ行を並べた [[0] * 3 for _ in range(3)]
関数を呼ぶと引数のリストが変わる 関数内で引数を直接書き換えた 関数の先頭で .copy()
TypeError: cannot pickle ... コピーできない属性を持っている __deepcopy__ を定義する
ループ内の deepcopy で処理が遅い 毎回すべての要素をたどっている 必要な階層だけ内包表記でコピー

よくある質問

Q. copy.copy と list.copy() は何が違いますか? A. リストに対してはどちらも浅いコピーで、結果は同じです。list.copy() はリスト専用のメソッド、copy.copy は自作クラスを含むさまざまなオブジェクトに使える関数です。測定では速度の差もほぼありませんでした。

Q. とりあえず全部 deepcopy にしておけばよいですか? A. 動作としては独立したコピーが得られるので、小さなデータなら問題なく使えます。ただし測定では浅いコピーの数百倍の時間がかかったので、大きなデータを繰り返しコピーする処理では、必要な階層だけをコピーする書き方が向いています。

Q. 浅いコピーで十分かどうかは、どう見分けますか? A. 中身が数値・文字列・タプル(中も変更できない値)だけなら浅いコピーで十分です。迷ったら a[0] is b[0] のように is で内側を比べ、True なら共有されていると判断できます。

Q. copy.replace と dataclasses.replace はどちらを使えばよいですか? A. Python 3.13 以降なら、dataclass・namedtuple・日付などを同じ書き方で扱える copy.replace が便利です。3.12 以前でも動かすコードでは dataclasses.replace や _replace を使います。

まとめ

  • 代入(b = a)はコピーではなく、同じオブジェクトに別名を付けるだけ。
  • 浅いコピー(a[:]・a.copy()・copy.copy(a))は外側だけ新しく作り、内側は共有する。
  • 深いコピー(copy.deepcopy(a))は内側まですべて新しく作り、循環参照や共有参照の関係も保つ。
  • [[0] * 3] * 3 は全行が同じリストになる。 [[0] * 3 for _ in range(3)] で行ごとに作る。
  • id() や is で比べると、共有されているかどうかをすぐ確かめられる。
  • deepcopy は手書きのコピーの約9〜30倍の時間がかかった。 繰り返し呼ぶ処理では、必要な階層だけを内包表記でコピーする。
  • copy.replace は Python 3.13 以降。 __copy__・__deepcopy__・__replace__ でクラスごとの動きを決められる。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Python copyとdeepcopyの違い|浅い/深いコピー早見表」

株式会社RJCでは、Pythonのコピーや参照のしくみを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?