紛失届
LOST-002
届出人
免税店の在庫担当、オさん
なくしたもの
封印シールを貼ったかばん(タプル)が一つ。はかりは「封印されたかばんは開けられない」というエラーを出したのに、開けてみると中身が一つ増えていた。
解決の条件
エラーが出たのに中身が入ってしまった理由を突き止め、本当に開けられないかばんを作る。
ハヌルソム国際空港 手荷物センター
朝礼で、ド・ミラン班長が封印シールを一枚掲げてみせます。「これが貼られたかばんは、誰にも開けられない。なのに今日の届けは、封印されたかばんが膨らんだって話だ」
免税店の在庫担当者から届いたコードは短いものです。タプルに品物を入れようとしたら TypeError が出ました。ところがタプルをもう一度表示してみると、品物が入っていたのです。班長が言います。「封印は外側のかばんに貼っただけ。中のかばんまで封印したわけじゃない」
オブジェクトは二種類に分かれます。作ったあとで中身を変えられるものがミュータブル(mutable)、変えられないものがイミュータブル(immutable)です。
| 種類 | 代表的な型 | 空港でいえば |
|---|---|---|
| ミュータブル | list、dict、set | ファスナーが開くかばん。出し入れしても同じかばんのまま。 |
| イミュータブル | int、float、str、tuple、frozenset、bool、None | 封印されたかばん。中身を変えたければ、新しいかばんを詰め直すしかない。 |
イミュータブルなオブジェクトには、中身を変える手段がそもそもありません。
word = "gate"
print(word.upper(), word)
# GATE gate
word[0] = "G"
# TypeError: 'str' object does not support item assignmentfs = frozenset({1, 2})
fs.add(3)
# AttributeError: 'frozenset' object has no attribute 'add'word.upper() は word を書き換えません。大文字になった新しい文字列を作って返します。文字列のメソッドはすべてこう動きます。
では n = n + 1 は何でしょう? 整数10というかばんを書き換えたのではありません。11という新しい整数オブジェクトを得て、タグ n をそちらへ付け替えたのです。イミュータブルなオブジェクトの「変更」は、いつでも新しいオブジェクト+再束縛です。
下のタグボードは、整数も一つずつかばんとして描きます。3行目のあと、n の矢印がどこへ行くか見てください。
タグボード
ステップ 1 / 0
次に実行する行: 0
実習台を準備中…
いま変わったもの
封印されたかばん(イミュータブル)
+= の二つの顔 (10分)a += b は、見た目には一つの操作です。しかしPythonはこう処理します。
a の型に __iadd__(インプレース加算)メソッドがあれば、それを呼びます。かばんをその場で変え、同じかばんを返します。a = a + b と同じように __add__ で新しいオブジェクトを作り、タグを付け替えます。print(hasattr(list, "__iadd__"), hasattr(int, "__iadd__"))
# True False
print(hasattr(str, "__iadd__"), hasattr(tuple, "__iadd__"))
# False Falseリストはミュータブルなので __iadd__ を持っています。イミュータブルな型にはありません。封印されたかばんは、その場で変えられないからです。そのため、同じ += がこう分かれます。
a = [1, 2]
b = a
a += [3]
print(a, b, a is b)
# [1, 2, 3] [1, 2, 3] True
a = (1, 2)
b = a
a += (3,)
print(a, b, a is b)
# (1, 2, 3) (1, 2) False
a = "T1"
b = a
a += "-A"
print(a, b, a is b)
# T1-A T1 False| 型 | a += x がすること | 別のタグ b から見えるか |
|---|---|---|
list | __iadd__:同じかばんに extend | 見える |
tuple、str、int | __add__:新しいオブジェクトを作り、a だけを付け替える | 見えない |
1限目の a = a + [3] と、リストの a += [3] が違う結果になる理由がこれです。前者は新しいリスト、後者は同じリストです。set の |= や dict の |= も、リストと同じくその場で変えます。
三つの型を切り替えて動かしてみてください。
タグボード
ステップ 1 / 0
次に実行する行: 0
実習台を準備中…
いま変わったもの
封印されたかばん(イミュータブル)
ピンの案内
→ 関数でリストを受け取って x += [...] をすると、呼び出し側のリストまで変わります。整数や文字列なら外側はそのまま。同じ1行が型によって違う動きをすることを覚えておいてください(4限目)。
タプルがイミュータブルだというのは、正確には「タプルが入れている参照を変えられない」という意味です。各枠がどのオブジェクトを指すかは固定されます。けれども、その枠が指すオブジェクトがリストなら、リストそのものはいくらでも変わります。
t = (1, [2])
t[1].append(3)
print(t)
# (1, [2, 3])
t[1] = [9]
# TypeError: 'tuple' object does not support item assignment届出人のコードは append ではなく += を使っていました。このとき、奇妙なことが起こります。
t = (1, [2])
try:
t[1] += [3]
except TypeError as e:
print("TypeError:", e)
print(t)
# TypeError: 'tuple' object does not support item assignment
# (1, [2, 3])エラーが出たのに、リストは変わっています。t[1] += [3] をばらして書くと、二段階になるからです。
t = (1, [2])
tmp = t[1]
tmp = tmp.__iadd__([3]) # 段階1:リストをその場で変える(成功)
print(t)
# (1, [2, 3])
t[1] = tmp # 段階2:タプルの枠に入れ直す(失敗)
# TypeError: 'tuple' object does not support item assignment段階1で、リストはすでに変わっています。段階2の枠への代入が失敗してエラーになりますが、元に戻す手段はありません。はかりが「封印されたかばんは開けられない」と叫びながら、品物を入れてしまった理由です。
タグボード
ステップ 1 / 0
次に実行する行: 0
実習台を準備中…
いま変わったもの
封印されたかばん(イミュータブル)
中まで封印されているかを確かめる簡単な方法があります。hash() が通るかを見ることです。ハッシュできるタプルは、中身もすべてハッシュできる(たいていはイミュータブルな)オブジェクトです(12限目)。
print(hash(("passport", ("snack",))) == hash(("passport", ("snack",))))
# True
hash(("passport", ["snack"]))
# TypeError: unhashable type: 'list'+= でつなぐコスト (6分)文字列はイミュータブルなので、s += "x" は原則として毎回新しい文字列を作ります。長さnの文字列を1文字ずつつなぐと、コピーする文字数は 1 + 2 + … + n と膨らみます。
def plus(n):
s = ""
for i in range(n):
s += "x"
return s
def plus_shared(n):
s = ""
for i in range(n):
last = s # 同じ文字列にタグがもう一枚付いている
s += "x"
return s
def join(n):
return "".join(["x" for i in range(n)])
print(plus(5) == plus_shared(5) == join(5))
# TrueCPythonでは、文字列に付いているタグが一枚だけなら、+= がこっそりその場で伸ばす最適化をします。そのため plus は思ったより速いのです。しかしタグがもう一枚付いただけで、この最適化は消えます。このPC(CPython 3.14.2)で測った例です。
| 繰り返し回数 | plus | plus_shared | join |
|---|---|---|---|
| 100,000 | 約6ms | 約120ms | 約3ms |
| 200,000 | 約12ms | 約460ms | 約5ms |
plus_shared は回数が2倍になると、時間が4倍近くに増えました。コピー量が2乗で増えている様子です。この最適化は言語が約束したものではないので、ループで文字列を集めるときは、リストに入れておいて "".join() で一度につなぎます。
is を使わない理由 (7分)イミュータブルなオブジェクトは変わることがないので、同じ値なら一つのオブジェクトを皆で共有しても安全です。CPythonはこれを利用して、よく使うオブジェクトをあらかじめ作っておきます。
小さな整数のキャッシュ。CPythonでは、-5から256までの整数を起動時に作っておき、いつも同じオブジェクトを返します。
print(int("256") is int("256"), int("257") is int("257"))
# True False
print(int("-5") is int("-5"), int("-6") is int("-6"))
# True Falseint("…") で実行中に整数を作っているのはわざとです。コードに 257 is 257 のようにリテラルを直接書くと、コンパイラが同じ定数を一つにまとめて True になることがあります。Pythonもこうした比較には警告を出します。
x = 256
print(x is 256)
# SyntaxWarning: "is" with 'int' literal. Did you mean "=="?
# True文字列のインターン。CPythonは、名前のような形の文字列定数を一つのオブジェクトにまとめて使います(インターン)。実行中に作った文字列は、値が同じでも別のオブジェクトかもしれません。sys.intern() を使えば、同じ値の文字列を一つのオブジェクトにそろえられます。
import sys
a = "gate"
b = "".join(["ga", "te"])
print(a == b, a is b)
# True False
print(sys.intern(b) is a)
# True結果がキャッシュやインターンの有無で変わるので、is で値を比べると、ある値では正しく、別の値では間違います。値はいつでも == で、is は None のように一つしかないオブジェクトにだけ使います。
ピンの案内
→ テストで小さな数だけを確かめていると、x is 100 がいつも True なのでバグが隠れます。本番で1000が来た瞬間に False になるんです。数値と文字列は == で比べましょう。
届出人のコードと、本当に開けられないかばんを作る方法です。
sealed = ("passport", ["snack"])
try:
sealed[1] += ["perfume"]
except TypeError as e:
print("scale:", e)
print(sealed)
# scale: 'tuple' object does not support item assignment
# ('passport', ['snack', 'perfume'])
sealed = ("passport", ("snack",)) # 中もタプルにする
try:
sealed[1] += ("perfume",)
except TypeError as e:
print("scale:", e)
print(sealed)
# scale: 'tuple' object does not support item assignment
# ('passport', ('snack',))__iadd__ が先に成功し、枠への代入だけが失敗しました。__iadd__ がないので新しいタプルを作るだけで、枠への代入で失敗します。元のかばんはそのままです。班長は新しい封印シールを中のかばんにも貼りながら言います。「封印は外側で終わりじゃない。中に入っているかばんまで見ないと」
解決報告 — LOST-002。タプルが固定するのは入れている参照だけで、中のリストはミュータブルだった。t[1] += [...] はその場での拡張のあと、枠への代入で失敗。中までイミュータブル(タプル)にして解決。
x = 1000、y = x、x += 1 を実行したあと、y は何でしょう? タグの図を描いてから確かめてください。s = {1}、t = s、s |= {2} を実行すると、t はどうなるでしょう? frozenset({1}) に変えても試してみてください。t = ([],) のとき、t[0] += [1] と t[0].extend([1]) はそれぞれどう終わるでしょう? 実行後に t も表示してみてください。班長の業務手帳
ミュータブル:list, dict, set。イミュータブル:int, float, str, tuple, frozenset。
イミュータブルなオブジェクトの「変更」は、いつでも新しいオブジェクトを作ってタグを付け替えること。
+= は __iadd__ があればその場で(list)、なければ新しいオブジェクト(int, str, tuple)。
タプルが固定するのは入れている参照だけ。中のリストは変わる。t[1] += [...] はエラーを出しながらも変える。
ループで文字列を集めるときは、リストに入れて "".join()。
小さな整数のキャッシュとインターンはCPythonの事情。値の比較はいつでも ==。
新しい授業・講座のお知らせを受け取る
この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。
R
ReflexKR
触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。
ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する
アフィリエイトと支援について© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
質問と回答
この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。