R

ReflexKR

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

時間 2:30–3:20

かばんを渡すとき:関数と引数

紛失届

LOST-004

呼ぶたびに前の注文がついてくる

届出人

フードデリバリーアプリのスタートアップの開発者、ジンさん

なくしたもの

新しい注文リスト。注文リストを作る関数を呼ぶたびに、前のお客さんの注文が積み重なったまま出てくる。

解決の条件

前の注文がどこに残っていたのかを突き止め、呼ぶたびに空のリストから始まるようにする。

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

昼休み、スタートアップの開発者がノートパソコンを抱えて駆け込んできます。最初のお客さんの注文はコーヒー一杯だったのに、二人目のお客さんのレシートにコーヒーとベーグルが一緒に印刷されたというのです。

ド・ミラン班長が、関数定義の1行を指でたどります。「かばんを渡すときは、かばんじゃなくてタグがもう一枚付くんだ。それに、この空のかばんは、関数を作ったときに一度だけ買ったものだね」

4-1. 関数はかばんにタグをもう一枚付ける (11分)

関数を呼んでも、引数として渡したオブジェクトはコピーされません。関数の中の引数名が、同じオブジェクトにタグとしてもう一枚付くのです。これをオブジェクト参照渡し(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

実習台を準備中…

いま変わったもの

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

bag.append では、pack 枠の bag と module の mine が同じかばんを指し、そのかばんが変わります。bag = bag + [...] では bag だけが新しいかばんへ移り、枠が消えるとその新しいかばんも一緒に捨てられます。

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はいつも同じことをしています。引数というタグを、受け取ったオブジェクトにもう一枚付けるだけ。外側から見えるかどうかは、関数がそのオブジェクトを変えたのか、タグを付け替えただけなのかで決まります。

4-2. デフォルト引数は定義したときに一度だけ作られる (11分)

届出人の関数を縮めると、こうなります。

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

実習台を準備中…

いま変わったもの

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

orders=[] では、function かばんの defaults タプルがリストを一つつかんだままなので、2回の呼び出しが同じリストに入れます。orders=None では defaults には None しかなく、呼び出しのたびに新しいリストができます。

直し方は決まっています。デフォルト値には 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__ に別にぶら下がります。

4-3. 変える関数と、新しく返す関数 (7分)

関数は二種類に分けて考えると楽です。受け取ったかばんをその場で変える関数と、元はそのままにして新しいかばんを返す関数です。

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] False

Pythonには慣習があります。その場で変える関数やメソッドは 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

自分で関数を作るときも、どちらか一方を選びましょう。変えたうえに返しもすると、呼び出し側は元が変わったことに気づかずに使ってしまいがちです。

4-4. *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

実習台を準備中…

いま変わったもの

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

pack 枠の items は新しいタプル、labels は新しい dict です。タプルの最初の枠は外側の bag と同じかばんを指しているので、2行目の append が外側からも見えます。

受け取ったミュータブルなオブジェクトを長く保管する関数は、特に注意が必要です。呼び出し側があとでそのオブジェクトを変えると、保管しておいたものも一緒に変わります。

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限目)。中身が文字列なので、これで十分です。逆に、受け取ったものを少し読むだけの関数なら、コピーする必要はありません。

4-5. 戻り値もタグの付け替え (4分)

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 9

return のない関数は None を返します。4-3の「変える関数」が None を返すのも、このためです。

事件解決 (10分)

届出人の関数と、直した関数です。

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 の番兵で呼び出しごとに新しいリストを作って解決。

練習問題

  1. def add(x, seen=set()) が seen.add(x) のあと len(seen) を返すとき、add(1)、add(2)、add(1) の結果を予想し、add.__defaults__ を確かめてください。
  2. def clear(lst): lst = [] と def clear(lst): lst.clear() は、外側のリストにそれぞれどんな影響を与えるでしょう?
  3. リストを受け取り、元は変えずに並べ替えた新しいリストを返す関数を、二通りの方法(sorted、コピーしてから sort)で作ってみてください。

班長の業務手帳

  • 引数渡しはオブジェクト参照渡し。引数は同じオブジェクトにもう一枚付くタグ。

  • 関数の中でオブジェクトを変えると外から見え、再束縛すると見えない。

  • デフォルト引数は def のときに一度だけ計算され、__defaults__ に残る。ミュータブルなデフォルト値の代わりに None。

  • その場で変える関数は None を返す(sort 対 sorted)。

  • *args・**kwargs は新しいタプル・dict だが、中のオブジェクトは同じ。長く保管するものは防御的コピー。

  • return もタグの付け替え。複数の値の返却はタプル一つ。

← 前へ

3限目 · かばんの中のかばん:コンテナとコピー

次へ →

5限目 · 区域と通行証:スコープ

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

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

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

質問と回答

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

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

R

ReflexKR

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

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

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

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

Built with Reflex