紛失届
LOST-003
届出人
中学校の修学旅行の引率教員、ユン先生
なくしたもの
クラスごとの持ち物セット。基本セットを「複製」して1組と2組に配ったのに、1組のかばんに水をこぼしたら、2組のかばんのタオルまで濡れた。
解決の条件
二つのクラスのセットがどこまで同じかばんだったかを突き止め、最後まで別々のセットを作る。
ハヌルソム国際空港 手荷物センター
午前十時、引率の先生が濡れたタオルの写真を持ってセンターにやって来ます。セットは確かに copy() で複製したのに、二つのクラスのタオルが一緒に濡れたというのです。
ド・ミラン班長がセットをひっくり返してみせます。外側のかばんは二つあります。ところが外側のかばんの中には、品物ではなくタグが差し込まれています。「外側のかばんだけ新しく買ったんだね。中に差したタグは、同じタオルのかばんを指している」
リスト、タプル、dict、set のようなコンテナは、オブジェクトそのものを入れてはいません。オブジェクトを指す参照、つまりタグを入れています。1限目で名前がタグだったように、リストの各枠もタグです。
そのため、同じオブジェクトが複数の枠に同時に入ることがあります。
row = [0, 0]
pair = [row, row]
row.append(7)
print(pair)
# [[0, 0, 7], [0, 0, 7]]
print(pair[0] is pair[1])
# Trueこの性質がいちばんよく事故を起こすのが、2次元リストです。
grid = [[0] * 3] * 3
grid[0][0] = 9
print(grid)
# [[9, 0, 0], [9, 0, 0], [9, 0, 0]]
print(grid[0] is grid[1] is grid[2])
# True[0] * 3 はリストを一つ作ります。外側の * 3 はそのリストを3回コピーするのではなく、同じリストを指すタグを3回差し込みます。かばんは一つ、タグは三つです。
一方、内側の [0] * 3 は問題ありません。どの枠も同じ整数 0 を指していますが、整数はイミュータブルなので、row[0] = 9 はその枠のタグを9に付け替えるだけです。共有が問題になるのは、ミュータブルなオブジェクトを共有するときです。
直し方は、行ごとに新しいリストを作る内包表記です。
grid = [[0] * 3 for _ in range(3)]
grid[0][0] = 9
print(grid)
# [[9, 0, 0], [0, 0, 0], [0, 0, 0]]タグボード
ステップ 1 / 0
次に実行する行: 0
実習台を準備中…
いま変わったもの
封印されたかばん(イミュータブル)
リストをコピーするよくある方法は五つあります。どれも結果は同じです。
import copy
kit = [["water", "map"], ["snack"]]
copies = [kit[:], list(kit), kit.copy(), copy.copy(kit)]
print([c is kit for c in copies])
# [False, False, False, False]
print([c[0] is kit[0] for c in copies])
# [True, True, True, True]これが浅いコピー(shallow copy)です。外側のかばんは新しく作り、中に差してあるタグはそのまま差し替えます。そのため、外側のかばんを変える操作と、内側のかばんを変える操作とで、結果が違ってきます。
a = kit[:]
a.append(["hat"]) # 外側のかばんに枠を追加:元はそのまま
print(len(kit), len(a))
# 2 3
a[0].append("towel") # 内側のかばんを変える:元からも見える
print(kit[0])
# ['water', 'map', 'towel']dict も同じです。d.copy()、dict(d)、{**d} はどれも浅いコピーです。
rooms = {"class1": ["water"], "class2": ["map"]}
dup = rooms.copy()
dup["class3"] = [] # 新しいキー:元にはない
dup["class1"].append("tea") # 共有されたリスト:元からも見える
print(rooms)
# {'class1': ['water', 'tea'], 'class2': ['map']}下で、=、浅いコピー、深いコピーを同じ入れ子のリストに試してみてください。4行目は内側のかばんを変え、5行目は外側のかばんに枠を足します。
タグボード
ステップ 1 / 0
次に実行する行: 0
実習台を準備中…
いま変わったもの
封印されたかばん(イミュータブル)
ピンの案内
→ 浅いコピーは「1層だけ」新しく作ります。中身が数値や文字列だけなら浅いコピーで十分。リストや dict が入っているなら、深いコピーが必要か考えてみてください。
copy.deepcopy は、かばんの中のかばん、さらにその中のかばんまでたどっていき、すべてを新しく作ります。
import copy
kit = [["water", "map"], ["snack"]]
deep = copy.deepcopy(kit)
deep[0].append("rain")
print(kit[0], deep[0], deep[0] is kit[0])
# ['water', 'map'] ['water', 'map', 'rain'] False最後までたどるコピーには、落とし穴が二つあります。同じかばんに二度出会う場合と、かばんが自分自身を入れていて終わりがない場合です。deepcopy は memo という台帳でどちらも解決します。コピー済みのオブジェクトを {id(元): コピー} として書き留めておき、また出会ったら台帳のコピーを使います。
x = [1]
pair = [x, x]
p2 = copy.deepcopy(pair)
print(p2[0] is p2[1], p2[0] is x)
# True False
memo = {}
copy.deepcopy(pair, memo)
print(id(x) in memo)
# True元で同じかばんだった二つの枠は、コピーでも同じかばんです。共有の関係までそのままコピーされます。
自分自身を入れたリストでも、無限にたどり続けることはありません。こうしたリストを表示すると、Pythonは [...] と省略して書きます。
a = []
a.append(a)
print(a)
# [[...]]
b = copy.deepcopy(a)
print(b[0] is b, b is a)
# True Falseタグボード
ステップ 1 / 0
次に実行する行: 0
実習台を準備中…
いま変わったもの
封印されたかばん(イミュータブル)
__copy__、__deepcopy__ メソッドを定義して、コピーのしかたを自分で決められます。そういうものがある、と知っておけば十分です。イミュータブルなオブジェクトは変わることがないので、コピーを作る理由がありません。copy は元のオブジェクトをそのまま返します。
import copy
t = (1, 2)
print(copy.copy(t) is t, copy.deepcopy(t) is t)
# True True
s = "gate"
print(copy.deepcopy(s) is s)
# Trueただし、タプルの中にミュータブルなオブジェクトがあると話は別です。浅いコピーは相変わらず元を返しますが、深いコピーは中のリストを新しく作らなければならないので、新しいタプルを作ります。
t2 = (1, [2])
print(copy.copy(t2) is t2)
# True
dt = copy.deepcopy(t2)
print(dt is t2, dt == t2, dt[1] is t2[1])
# False True False深いコピーで作ったリストの中の文字列も同じです。リストは新しくても、その中の "map" は元と同じ文字列オブジェクトです。
kit = [["map"], ["snack"]]
deep = copy.deepcopy(kit)
print(deep[0] is kit[0], deep[0][0] is kit[0][0])
# False True深いコピーでも、封印されたかばんは詰め直しません。変わりようのないものは、共有しても安全だからです。
copy.replace():一部だけ変えた新しいオブジェクト (5分)イミュータブルなオブジェクトを「直したい」ときは、一部だけ変えた新しいオブジェクトを作ります。3.13からは copy.replace() が、この作業を一つのやり方でこなしてくれます。frozen=True のデータクラス、namedtuple、datetime.date などのオブジェクトが対応しています。
import copy
from dataclasses import dataclass
from collections import namedtuple
@dataclass(frozen=True)
class Ticket:
gate: str
seat: str
tk = Ticket("A1", "12C")
tk2 = copy.replace(tk, seat="14F")
print(tk, tk2)
# Ticket(gate='A1', seat='12C') Ticket(gate='A1', seat='14F')
Pass = namedtuple("Pass", "gate seat")
print(copy.replace(Pass("A1", "12C"), gate="B7"))
# Pass(gate='B7', seat='12C')
tk.seat = "1A"
# dataclasses.FrozenInstanceError: cannot assign to field 'seat'元の tk はそのままで、変えたフィールドだけが違う新しいオブジェクトができます。対応していないオブジェクト(リストなど)に使うと、TypeError: replace() does not support list objects になります。
引率の先生のコードをもう一度見てみます。
import copy
kit = [["towel", "notebook"], ["raincoat"]]
class1 = kit.copy()
class2 = kit.copy()
print(class1 is class2, class1[0] is class2[0])
# False True
class1[0][0] = "wet towel"
print(class2[0])
# ['wet towel', 'notebook']外側のかばんは別々でしたが、最初の枠のタグは同じタオル袋を指していました。水は、その一つしかない袋にこぼれたのです。直し方は二つあります。
kit = [["towel", "notebook"], ["raincoat"]]
class1 = copy.deepcopy(kit)
class2 = copy.deepcopy(kit)
class1[0][0] = "wet towel"
print(class2[0], class1[0] is class2[0])
# ['towel', 'notebook'] False
def new_kit():
return [["towel", "notebook"], ["raincoat"]] # 呼ぶたびに新しいかばん
print(new_kit()[0] is new_kit()[0])
# Falseセットを作る関数を用意する方法は、コピー元そのものをなくします。呼ぶたびに一から詰め直すので、共有されるものがありません。
先生が帰ったあと、ピンが13番コンベヤーのX線写真を映し出します。幽霊かばんが二つ、互いの中に入っているように写っています。班長が画面に1行書き込みます。
a = []
b = [a]
a.append(b)
print(a)
# [[[...]]]「互いを抱え込んだかばん、か。タグがなくても片付けられない理由は、そこにあるのかもしれない」班長は写真をファイルに挟みます。
解決報告 — LOST-003。copy() は浅いコピーなので、内側の持ち物袋を二つのクラスで共有していた。copy.deepcopy、またはセットを新しく作る関数で解決。
board = [[]] * 2 に board[0].append("x") をすると、board はどうなるでしょう? 内包表記で直してみてください。d = {"a": [1]} に対して e = dict(d) をしたあと、e["a"].append(2) と e["b"] = [3] がそれぞれ d から見えるか予想してください。x = [1]、y = [x, x]、z = copy.deepcopy(y) のとき、z[0] is z[1] は? memo がなかったらどうなっていたかも考えてみてください。班長の業務手帳
コンテナはオブジェクトではなく参照(タグ)を入れる。同じオブジェクトが複数の枠に入りうる。
[[0] * 3] * 3 は同じリストが三つ。2次元リストは内包表記で。
浅いコピー(lst[:], list(), .copy(), copy.copy):外側だけ新しく、中は共有。
深いコピー(copy.deepcopy):最後まで新しく。memo で共有と循環を一度だけコピー。
イミュータブルなオブジェクトはコピーしても同じオブジェクト。中にミュータブルがあれば deepcopy だけが新しく作る。
copy.replace():イミュータブルなオブジェクトの一部だけを変えた新しいオブジェクト(3.13)。
新しい授業・講座のお知らせを受け取る
この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。
R
ReflexKR
触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。
ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する
アフィリエイトと支援について© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
質問と回答
この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。