紛失届
LOST-004
届出人
フードデリバリーアプリのスタートアップの開発者、ジンさん
なくしたもの
新しい注文リスト。注文リストを作る関数を呼ぶたびに、前のお客さんの注文が積み重なったまま出てくる。
解決の条件
前の注文がどこに残っていたのかを突き止め、呼ぶたびに空のリストから始まるようにする。
ハヌルソム国際空港 手荷物センター
昼休み、スタートアップの開発者がノートパソコンを抱えて駆け込んできます。最初のお客さんの注文はコーヒー一杯だったのに、二人目のお客さんのレシートにコーヒーとベーグルが一緒に印刷されたというのです。
ド・ミラン班長が、関数定義の1行を指でたどります。「かばんを渡すときは、かばんじゃなくてタグがもう一枚付くんだ。それに、この空のかばんは、関数を作ったときに一度だけ買ったものだね」
関数を呼んでも、引数として渡したオブジェクトはコピーされません。関数の中の引数名が、同じオブジェクトにタグとしてもう一枚付くのです。これをオブジェクト参照渡し(call by object reference、call by sharing)といいます。
mine = ["laptop"]
def show(bag):
print(bag is mine)
show(mine)
# Trueタグがもう一枚付いただけなので、1限目の二つの動作がそのまま当てはまります。
bag.append(...) のように中身を変えると、外側の名前からも見えます。同じかばんだからです。bag = ... と再束縛すると、関数の中のタグだけが移ります。外側の名前は元のかばんに付いたままです。def pack(bag):
bag.append("charger")
def swap(bag):
bag = ["new"]
bag.append("charger")
mine = ["laptop"]
pack(mine)
print(mine)
# ['laptop', 'charger']
swap(mine)
print(mine)
# ['laptop', 'charger']下のタグボードには、初めて呼び出し枠(フレーム)が登場します。関数を呼ぶと左側に pack の枠ができ、その中のタグ bag が外側の mine と同じかばんを指します。関数が終わると枠は消えます。
タグボード
ステップ 1 / 0
次に実行する行: 0
実習台を準備中…
いま変わったもの
封印されたかばん(イミュータブル)
2限目の += がここで再び出てきます。同じ += でも、型によって外側から見えたり見えなかったりします。
def bump(n):
n += 1 # int:新しいオブジェクト。関数の中のタグだけを付け替える
def extend(lst):
lst += [1] # list:__iadd__。同じかばんを変える
count, items = 0, []
bump(count)
extend(items)
print(count, items)
# 0 [1]Pythonには「外側の変数を書き換える引数」はありません。関数が外側に影響を与える道は、受け取ったミュータブルなオブジェクトを変えるか、値を返すかのどちらかだけです。
ピンの案内
→ 「値渡しか参照渡しか」で悩まなくて大丈夫。Pythonはいつも同じことをしています。引数というタグを、受け取ったオブジェクトにもう一枚付けるだけ。外側から見えるかどうかは、関数がそのオブジェクトを変えたのか、タグを付け替えただけなのかで決まります。
届出人の関数を縮めると、こうなります。
def f(x=[]):
x.append(1)
return x
print(f())
# [1]
print(f())
# [1, 1]
print(f.__defaults__)
# ([1, 1],)デフォルト値 [] は、関数を呼ぶときではなく、def 文が実行されるときに一度だけ計算されます。そうして作られたリストは関数オブジェクトの __defaults__ タプルにぶら下がり、関数が生きているあいだ残ります。引数を省略して呼ぶたびに、引数 x はその一つしかないリストに付くのです。
print(f() is f.__defaults__[0])
# True下のタグボードで、2回の呼び出しを追ってみてください。呼び出し枠はできては消えますが、defaults にぶら下がったリストはずっと残り、注文が積み重なっていきます。
タグボード
ステップ 1 / 0
次に実行する行: 0
実習台を準備中…
いま変わったもの
封印されたかばん(イミュータブル)
直し方は決まっています。デフォルト値には None を置き、関数の中でそのつど新しいリストを作ります。
def add_order(item, orders=None):
if orders is None:
orders = []
orders.append(item)
return orders
print(add_order("coffee"), add_order("bagel"))
# ['coffee'] ['bagel']
print(add_order.__defaults__)
# (None,)None 自体が意味のある値で、「引数が渡されなかった」と区別しなければならないときは、誰も使わないオブジェクトを一つ作って番兵(sentinel)にします。世界に一つしかないオブジェクトなので、is で比べます。
_MISSING = object()
def lookup(key, default=_MISSING):
if default is _MISSING:
return f"no default for {key}"
return default
print(lookup("gate"), lookup("gate", None))
# no default for gate None[]、{}、set() だけではありません。def のときに一度だけ計算されるという規則は、すべてのデフォルト値に同じように当てはまります。f.__kwdefaults__ に別にぶら下がります。関数は二種類に分けて考えると楽です。受け取ったかばんをその場で変える関数と、元はそのままにして新しいかばんを返す関数です。
nums = [3, 1, 2]
print(nums.sort(), nums)
# None [1, 2, 3]
nums = [3, 1, 2]
out = sorted(nums)
print(out, nums, out is nums)
# [1, 2, 3] [3, 1, 2] FalsePythonには慣習があります。その場で変える関数やメソッドは None を返します。list.sort()、list.append()、list.reverse()、dict.update()、random.shuffle() はどれもそうです。変わったのが元のオブジェクトだという事実を隠さないための約束です。
そのため、こんなミスが起こります。
nums = [3, 1, 2]
nums = nums.sort()
print(nums)
# Noneその場で変える(None を返す) | 新しいオブジェクトを返す |
|---|---|
lst.sort() | sorted(lst) |
lst.reverse() | lst[::-1]、list(reversed(lst)) |
lst.append(x)、lst.extend(xs) | lst + [x] |
d.update(other) | d | other |
自分で関数を作るときも、どちらか一方を選びましょう。変えたうえに返しもすると、呼び出し側は元が変わったことに気づかずに使ってしまいがちです。
*args と **kwargs、そして防御的コピー (7分)*args は余った位置引数を新しいタプルに、**kwargs は余ったキーワード引数を新しい dict に集めます。ただし、その中に差してあるのは、渡したオブジェクトのタグです。
def g(*args, **kwargs):
return args, kwargs
items = ["a"]
opts = {"tag": items}
a, k = g(items, **opts)
print(type(a), type(k))
# <class 'tuple'> <class 'dict'>
print(a[0] is items, k is opts, k["tag"] is items)
# True False True**opts で展開して渡しても、関数が受け取る kwargs は新しい dict です。だから関数が kwargs にキーを足しても、opts はそのままです。けれども、値として入っていたリストは同じオブジェクトです。
タグボード
ステップ 1 / 0
次に実行する行: 0
実習台を準備中…
いま変わったもの
封印されたかばん(イミュータブル)
受け取ったミュータブルなオブジェクトを長く保管する関数は、特に注意が必要です。呼び出し側があとでそのオブジェクトを変えると、保管しておいたものも一緒に変わります。
def make_flight(code, passengers):
return {"code": code, "passengers": passengers}
names = ["Ari", "Bo"]
f1 = make_flight("HS1", names)
names.append("Cy")
print(f1["passengers"])
# ['Ari', 'Bo', 'Cy']こういうときは、受け取った瞬間に自分の分を新しく作ります。これを防御的コピー(defensive copy)といいます。
def make_flight(code, passengers):
return {"code": code, "passengers": list(passengers)}
names = ["Ari", "Bo"]
f1 = make_flight("HS1", names)
names.append("Cy")
print(f1["passengers"])
# ['Ari', 'Bo']list(passengers) は浅いコピーです(3限目)。中身が文字列なので、これで十分です。逆に、受け取ったものを少し読むだけの関数なら、コピーする必要はありません。
return もオブジェクトをコピーしません。関数の中で作ったかばんを呼び出し側に渡し、呼び出し側の名前がそのかばんにタグを付けます。関数の枠が消えても、外側のタグがあるので、かばんは残ります。
def build():
bag = ["boarding pass"]
return bag
b = build()
print(b)
# ['boarding pass']複数の値を返しているように見える return a, b は、タプルを一つ返しています。受け取る側でアンパックすると、タグが一枚ずつ付きます。
def stats(xs):
return min(xs), max(xs)
res = stats([4, 9, 2])
print(res, type(res))
# (2, 9) <class 'tuple'>
lo, hi = stats([4, 9, 2])
print(lo, hi)
# 2 9return のない関数は None を返します。4-3の「変える関数」が None を返すのも、このためです。
届出人の関数と、直した関数です。
def new_order(item, cart=[]):
cart.append(item)
return cart
print(new_order("coffee"))
# ['coffee']
print(new_order("bagel"))
# ['coffee', 'bagel']
print(new_order.__defaults__)
# (['coffee', 'bagel'],)
def new_order(item, cart=None):
if cart is None:
cart = []
cart.append(item)
return cart
print(new_order("coffee"), new_order("bagel"))
# ['coffee'] ['bagel']前の注文は、関数オブジェクトの __defaults__ にぶら下がったリストの中にありました。サーバーは関数を一度定義したら呼び続けるので、そのリストはサーバーが止まるまで、お客さんごとの注文を集め続けたのです。
開発者が帰ったあと、班長が一言付け加えます。「タグは関数が終われば外れる。でも、関数オブジェクトがつかんでいるかばんは外れない。関数もかばんだからね」
解決報告 — LOST-004。デフォルト引数 cart=[] は def のときに一度だけ作られ、__defaults__ に残っていた。すべての呼び出しが同じリストを共有。None の番兵で呼び出しごとに新しいリストを作って解決。
def add(x, seen=set()) が seen.add(x) のあと len(seen) を返すとき、add(1)、add(2)、add(1) の結果を予想し、add.__defaults__ を確かめてください。def clear(lst): lst = [] と def clear(lst): lst.clear() は、外側のリストにそれぞれどんな影響を与えるでしょう?sorted、コピーしてから sort)で作ってみてください。班長の業務手帳
引数渡しはオブジェクト参照渡し。引数は同じオブジェクトにもう一枚付くタグ。
関数の中でオブジェクトを変えると外から見え、再束縛すると見えない。
デフォルト引数は def のときに一度だけ計算され、__defaults__ に残る。ミュータブルなデフォルト値の代わりに None。
その場で変える関数は None を返す(sort 対 sorted)。
*args・**kwargs は新しいタプル・dict だが、中のオブジェクトは同じ。長く保管するものは防御的コピー。
return もタグの付け替え。複数の値の返却はタプル一つ。
新しい授業・講座のお知らせを受け取る
この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。
R
ReflexKR
触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。
ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する
アフィリエイトと支援について© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
質問と回答
この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。