紛失届
LOST-006
届出人
搭乗案内放送担当のソさん
なくしたもの
搭乗口を案内する関数が三つ。ループで A1、B7、C3 用を作ったのに、三つとも C3 へ行くよう案内する。
解決の条件
三つの関数が何を持ち歩いているかを確かめ、関数ごとに自分の搭乗口を覚えさせる。
ハヌルソム国際空港 手荷物センター
朝の放送の直後、別々の搭乗口へ行くはずの乗客三人が、そろって C3 の前に立っています。ソさんは、案内関数を三つ作ったのだから案内も三通りになるはずだと言います。
班長が機内持ち込みかばんを一つ掲げてみせます。「関数もかばんを持ち歩いている。問題は、その中に入っているのが値なのか、名札なのかだよ」今日は、関数が持ち歩く小さなかばん、クロージャを開けてみます。
1限目で見たように、関数もオブジェクトです。名前に結び付け、リストに入れ、ほかの関数に渡し、返すことができます。
def make_tag(owner):
def tag():
return "owner: " + owner
return tag
t = make_tag("kim")
print(t())
# owner: kim
print(t.__name__, t.__qualname__)
# tag make_tag.<locals>.tagmake_tag を呼ぶたびに def tag(): が実行され、新しい関数オブジェクトが作られます。t() を呼んだ時点で、make_tag はもう終わっています。5限目の規則どおりなら、make_tag のローカル名 owner も一緒に消えているはずです。t() は "kim" を覚えています。終わった関数の名前を、内側の関数はどうやって持っているのでしょう?__closure__ (9分)内側の関数が外側の関数の名前を使うと、Pythonはその名前をセル(cell)という小さなかばんに入れます。外側の関数の枠と内側の関数が、同じセルを指します。外側の関数が終わって枠が消えても、内側の関数がセルを持っているので、セルとその中の値は残ります。このセルの束を持ち歩く関数がクロージャです。
def make_tag(owner):
def tag():
return "owner: " + owner
return tag
t = make_tag("kim")
print(t.__code__.co_freevars)
# ('owner',)
print(type(t.__closure__[0]).__name__)
# cell
print(t.__closure__[0].cell_contents)
# kim
print(make_tag.__code__.co_cellvars)
# ('owner',)| 属性 | 意味 |
|---|---|
inner.__code__.co_freevars | 内側の関数が外側から借りている名前(自由変数) |
inner.__closure__ | その名前ごとに一つずつ並んだ、セルのタプル |
cell.cell_contents | セルがいま指しているオブジェクト |
outer.__code__.co_cellvars | 外側の関数の側でセルにしておいた名前 |
タグボードでは、make_tag の実行中、枠の owner はセルを開けて見た値で表示されます。枠が消えたあとも、function tag の owner の行が cell を通して 'kim' をつかんでいるか見てください。
タグボード
ステップ 1 / 0
次に実行する行: 0
実習台を準備中…
いま変わったもの
封印されたかばん(イミュータブル)
def make_tag(owner):
notes = ["memo"] * 1000
def tag():
return owner
return tag
print(make_tag("kim").__code__.co_freevars)
# ('owner',)ピンの案内
→ クロージャが持っているのは値のコピーではなく、セル、つまり名札がぶら下がる場所です。だから外側でその名前を付け替えると、クロージャが見る値も変わります。
届出人のコードです。
def make_guides(gates):
guides = []
for gate in gates:
guides.append(lambda: gate)
return guides
gs = make_guides(["A1", "B7", "C3"])
print([g() for g in gs])
# ['C3', 'C3', 'C3']
print(gs[0].__closure__[0] is gs[2].__closure__[0])
# Truegate は make_guides の中の一つの名前で、セルも一つです。lambda は、どれもそのたった一つのセルを持っていきます。セルの中の値は、ループが終わった時点で "C3" です。lambda の本体の gate は、呼ばれたときにセルを開けて見ます。作ったときに値を書き留めてはいません。これを遅延束縛(late binding)といいます。リスト内包表記で書いても同じです。
fs = [lambda: i for i in range(3)]
print([f() for f in fs])
# [2, 2, 2]
print(fs[0].__closure__[0] is fs[1].__closure__[0])
# Trueループがなくても同じことが起こります。関数を作ったあとで外側の名前を付け替えると、関数が見る値も変わります。
def make():
gate = "A1"
def guide():
return gate
gate = "Z9"
return guide
print(make()())
# Z9下のタグボードで、三つの関数の矢印が一つのセルに集まる様子を見てください。直した版では、関数ごとに自分の defaults タプルを持ちます。
タグボード
ステップ 1 / 0
次に実行する行: 0
実習台を準備中…
いま変わったもの
封印されたかばん(イミュータブル)
直し方は、「作った瞬間の値」を関数に別に持たせることです。
fs = [lambda i=i: i for i in range(3)]
print([f() for f in fs])
# [0, 1, 2]
from functools import partial
def guide(gate):
return "go to " + gate
guides = [partial(guide, gate) for gate in ["A1", "B7", "C3"]]
print([g() for g in guides])
# ['go to A1', 'go to B7', 'go to C3']lambda i=i: i:デフォルト引数は定義するときに一度だけ計算されます(4限目)。その瞬間の i が関数の __defaults__ に入ります。functools.partial(guide, gate):関数とその瞬間の引数を束ねた、新しい呼び出し可能オブジェクトを作ります。デフォルト引数の小技より意図がはっきりします。f(5) のように値を上書きできてしまうという欠点があります。nonlocal カウンター (7分)セルは読むだけのものではありません。nonlocal(5限目)で宣言すれば、内側の関数がセルの中のタグを付け替えられます。すると関数は、呼ぶたびに引き継がれる状態を持つようになります。
def counter():
n = 0
def tick():
nonlocal n
n += 1
return n
return tick
c = counter()
c()
c()
print(c(), c.__closure__[0].cell_contents)
# 3 3
c2 = counter()
print(c2(), c())
# 1 4counter() を呼ぶたびに新しいセルができます。c と c2 は互いの数を知りません。nonlocal n がないと、n += 1 が n をローカル名にしてしまい、UnboundLocalError になります(5限目と同じ原因)。タグボード
ステップ 1 / 0
次に実行する行: 0
実習台を準備中…
いま変わったもの
封印されたかばん(イミュータブル)
同じことはクラスでもできます。状態はインスタンスの属性に入ります(7限目)。
class Counter:
def __init__(self):
self.n = 0
def tick(self):
self.n += 1
return self.n
k = Counter()
k.tick()
print(k.tick(), k.n)
# 2 2| クロージャ | クラス | |
|---|---|---|
| 状態のある場所 | セル | インスタンスの __dict__ |
| 外から見る方法 | __closure__ 経由のみ | k.n ですぐ |
| 向いている場面 | 動作が一つ、状態が少し | 動作が複数、状態も複数 |
関数を受け取って新しい関数を返すクロージャを、デコレーターといいます。元の関数は、内側の関数のセルに入ります。
def log(func):
def wrapper(*args, **kwargs):
print("call", func.__name__)
return func(*args, **kwargs)
return wrapper
@log
def board(seat):
return "seat " + seat
print(board("12A"))
# call board
# seat 12A
print(board.__name__)
# wrapper@log は board = log(board) と同じです。名前 board はいまや wrapper 関数を指し、元の board は wrapper のセル func に入っています。board.__name__ が wrapper になります。デバッグやドキュメントで困ります。functools.wraps の1行が、元の関数の名前やドキュメントなどを移し替えてくれます。
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("call", func.__name__)
return func(*args, **kwargs)
return wrapper
@log
def board(seat):
return "seat " + seat
print(board.__name__, board.__wrapped__.__name__)
# board boardデコレーターに引数を渡す方法や、クラスで作るデコレーターといった、もっと深い話はジェネレーター・デコレーターの講座で扱います。
関数ごとに、作ったときの搭乗口を持たせればよいのです。
from functools import partial
def announce(gate):
return "go to " + gate
def make_guides(gates):
return [partial(announce, gate) for gate in gates]
gs = make_guides(["A1", "B7", "C3"])
print([g() for g in gs])
# ['go to A1', 'go to B7', 'go to C3']
def make_guides_v2(gates):
return [lambda gate=gate: "go to " + gate for gate in gates]
print([g() for g in make_guides_v2(["A1", "B7", "C3"])])
# ['go to A1', 'go to B7', 'go to C3']次の放送から、三人の乗客はそれぞれ自分の搭乗口へ向かいます。ソさんが帰ったあと、班長が13番コンベヤーの記録をめくりながらつぶやきます。「終わった関数の持ち物でも、誰かが機内持ち込みかばんに入れて持っていれば捨てられない。あの二つのかばんも、誰かが握っているのかもしれない」
解決報告 — LOST-006。三つの案内関数がループ変数 gate のセル一つを共有していたため、呼ばれたときに最後の値 C3 を読んでいた(遅延束縛)。partial とデフォルト引数で作ったときの値を持たせて解決。
[lambda: i for i in range(3)] を [lambda: i * 10 for i in range(3)] に変えると、結果は? セルがいくつあるか __closure__ で確かめてください。counter() を直して、tick() と reset() の二つの関数を一緒に返すようにしてみてください。二つの関数が同じセルを使っているか、is で確かめてください。count_calls を作ってみてください。回数は wrapper の属性に置きますか、それともセルに置きますか? 両方試してみましょう。班長の業務手帳
関数もオブジェクト。関数を作って返すことができる。
内側の関数が使う外側の名前はセルに入り、関数は __closure__ でそのセルを持ち歩く。
クロージャが持っているのは値ではなくセル(名札の場所)。呼ばれたときに開ける:遅延束縛。
ループで作った関数たちはセル一つを共有する。直し方:lambda i=i: i、functools.partial。
nonlocal でセルの中のタグを付け替えると、状態を持つ関数になる。状態が多ければクラス。
デコレーターは関数を受け取って関数を返すクロージャ。functools.wraps を忘れずに。
新しい授業・講座のお知らせを受け取る
この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。
R
ReflexKR
触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。
ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する
アフィリエイトと支援について© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
質問と回答
この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。