- Python
Python copyとdeepcopyの違い|浅い/深いコピー早見表
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期連続の黒字決算。