R

ReflexKR

Reflex公式ドキュメント
名札の空港:Pythonのオブジェクトモデル
3限目

時間 1:40–2:30

かばんの中のかばん:コンテナとコピー

紛失届

LOST-003

1組のかばんにこぼした水が、2組のかばんを濡らした

届出人

中学校の修学旅行の引率教員、ユン先生

なくしたもの

クラスごとの持ち物セット。基本セットを「複製」して1組と2組に配ったのに、1組のかばんに水をこぼしたら、2組のかばんのタオルまで濡れた。

解決の条件

二つのクラスのセットがどこまで同じかばんだったかを突き止め、最後まで別々のセットを作る。

ハヌルソム国際空港 手荷物センター

午前十時、引率の先生が濡れたタオルの写真を持ってセンターにやって来ます。セットは確かに copy() で複製したのに、二つのクラスのタオルが一緒に濡れたというのです。

ド・ミラン班長がセットをひっくり返してみせます。外側のかばんは二つあります。ところが外側のかばんの中には、品物ではなくタグが差し込まれています。「外側のかばんだけ新しく買ったんだね。中に差したタグは、同じタオルのかばんを指している」

3-1. かばんの中にはタグが差してある (10分)

リスト、タプル、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

実習台を準備中…

いま変わったもの

封印されたかばん(イミュータブル)

[[0] * 3] * 3 では、外側のリストの三つの枠の矢印がすべて一つのかばんに集まります。内包表記は行ごとにかばんを一つずつ新しく作ります。

3-2. 浅いコピー:外側のかばんだけ新しく (10分)

リストをコピーするよくある方法は五つあります。どれも結果は同じです。

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

実習台を準備中…

いま変わったもの

封印されたかばん(イミュータブル)

= では外側のかばんも同じなので、4行目・5行目の両方が kit から見えます。kit[:] は外側だけが新しいかばんなので、4行目だけが見えます。copy.deepcopy は内側のリストまで新しく作るので、kit はそのままです。

ピンの案内

→ 浅いコピーは「1層だけ」新しく作ります。中身が数値や文字列だけなら浅いコピーで十分。リストや dict が入っているなら、深いコピーが必要か考えてみてください。

3-3. 深いコピー:最後まで新しく (9分)

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

実習台を準備中…

いま変わったもの

封印されたかばん(イミュータブル)

3行目のあと、リストの枠が自分自身を指します。deepcopy のコピーも、自分自身を指す新しいリストです。元のリストとはつながっていません。
  • 自作のクラスでは、__copy__、__deepcopy__ メソッドを定義して、コピーのしかたを自分で決められます。そういうものがある、と知っておけば十分です。
  • 深いコピーはオブジェクトのグラフ全体をたどるので、遅く、メモリも多く使います。必要な層だけを新しく作るほうがよい場合も多くあります。

3-4. 封印されたかばんはコピーしない (6分)

イミュータブルなオブジェクトは変わることがないので、コピーを作る理由がありません。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

深いコピーでも、封印されたかばんは詰め直しません。変わりようのないものは、共有しても安全だからです。

3-5. 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 になります。

事件解決 (10分)

引率の先生のコードをもう一度見てみます。

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、またはセットを新しく作る関数で解決。

練習問題

  1. board = [[]] * 2 に board[0].append("x") をすると、board はどうなるでしょう? 内包表記で直してみてください。
  2. d = {"a": [1]} に対して e = dict(d) をしたあと、e["a"].append(2) と e["b"] = [3] がそれぞれ d から見えるか予想してください。
  3. 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)。

← 前へ

2限目 · 封印されたかばん:ミュータブルとイミュータブル

次へ →

4限目 · かばんを渡すとき:関数と引数

新しい授業・講座のお知らせを受け取る

この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。

収集項目:メールアドレス・目的:この講座の新しい授業・公開のお知らせ・保有期間:解除のご依頼まで。他の目的には使わず、第三者にも提供しません。お知らせメールに返信いただければすぐに削除します。

質問と回答

この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。

リンクや書式なしで、そのままの文字で表示されます。

R

ReflexKR

触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。

ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する

アフィリエイトと支援について
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex