R

ReflexKR

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

時間 4:10–5:00

機内持ち込みかばん:クロージャ

紛失届

LOST-006

三人の乗客が全員同じ搭乗口へ

届出人

搭乗案内放送担当のソさん

なくしたもの

搭乗口を案内する関数が三つ。ループで A1、B7、C3 用を作ったのに、三つとも C3 へ行くよう案内する。

解決の条件

三つの関数が何を持ち歩いているかを確かめ、関数ごとに自分の搭乗口を覚えさせる。

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

朝の放送の直後、別々の搭乗口へ行くはずの乗客三人が、そろって C3 の前に立っています。ソさんは、案内関数を三つ作ったのだから案内も三通りになるはずだと言います。

班長が機内持ち込みかばんを一つ掲げてみせます。「関数もかばんを持ち歩いている。問題は、その中に入っているのが値なのか、名札なのかだよ」今日は、関数が持ち歩く小さなかばん、クロージャを開けてみます。

6-1. 関数を返す関数 (7分)

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>.tag
  • make_tag を呼ぶたびに def tag(): が実行され、新しい関数オブジェクトが作られます。
  • t() を呼んだ時点で、make_tag はもう終わっています。5限目の規則どおりなら、make_tag のローカル名 owner も一緒に消えているはずです。
  • ところが t() は "kim" を覚えています。終わった関数の名前を、内側の関数はどうやって持っているのでしょう?

6-2. 機内持ち込みかばん:セルと __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

実習台を準備中…

いま変わったもの

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

make_tag が終わるとその枠は消えますが、関数 tag が持っている cell が 'kim' をつかんだままです。
  • 機内持ち込みかばんに入るのは、内側の関数が実際に使う名前だけです。外側の関数のほかのローカル名は、関数が終われば一緒に片付けられます。
  • 逆に、入ったものは、その関数が生きているあいだずっと残ります。大きなリストを内側の関数が一度でも使えば、そのリストも一緒に残ります。
def make_tag(owner):
    notes = ["memo"] * 1000
    def tag():
        return owner
    return tag

print(make_tag("kim").__code__.co_freevars)
# ('owner',)

ピンの案内

→ クロージャが持っているのは値のコピーではなく、セル、つまり名札がぶら下がる場所です。だから外側でその名前を付け替えると、クロージャが見る値も変わります。

6-3. 遅延束縛:三人の乗客が全員 C3 へ行った理由 (10分)

届出人のコードです。

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])
# True
  • ループは区域を作りません(5限目)。3回回るあいだ、gate は 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

実習台を準備中…

いま変わったもの

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

lambda: gate では、三つの関数の gate がすべて同じ cell を指します。gate=gate に直すと、関数ごとに作ったときの値がデフォルト引数に入ります。

直し方は、「作った瞬間の値」を関数に別に持たせることです。

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) のように値を上書きできてしまうという欠点があります。

6-4. 状態を持つ関数: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 4
  • counter() を呼ぶたびに新しいセルができます。c と c2 は互いの数を知りません。
  • nonlocal n がないと、n += 1 が n をローカル名にしてしまい、UnboundLocalError になります(5限目と同じ原因)。

タグボード

ステップ 1 / 0

次に実行する行: 0

実習台を準備中…

いま変わったもの

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

tick を呼ぶたびに、cell の中の値が0から1、2へと変わります。整数はイミュータブルなので、整数が変わるのではなく、セルの中のタグが新しい整数へ付け替わっているのです。

同じことはクラスでもできます。状態はインスタンスの属性に入ります(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 ですぐ
向いている場面動作が一つ、状態が少し動作が複数、状態も複数

6-5. デコレーターをちょっと味見 (7分)

関数を受け取って新しい関数を返すクロージャを、デコレーターといいます。元の関数は、内側の関数のセルに入ります。

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

デコレーターに引数を渡す方法や、クラスで作るデコレーターといった、もっと深い話はジェネレーター・デコレーターの講座で扱います。

事件解決 (10分)

関数ごとに、作ったときの搭乗口を持たせればよいのです。

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 とデフォルト引数で作ったときの値を持たせて解決。

練習問題

  1. [lambda: i for i in range(3)] を [lambda: i * 10 for i in range(3)] に変えると、結果は? セルがいくつあるか __closure__ で確かめてください。
  2. counter() を直して、tick() と reset() の二つの関数を一緒に返すようにしてみてください。二つの関数が同じセルを使っているか、is で確かめてください。
  3. 関数が何回呼ばれたかを数えるデコレーター count_calls を作ってみてください。回数は wrapper の属性に置きますか、それともセルに置きますか? 両方試してみましょう。

班長の業務手帳

  • 関数もオブジェクト。関数を作って返すことができる。

  • 内側の関数が使う外側の名前はセルに入り、関数は __closure__ でそのセルを持ち歩く。

  • クロージャが持っているのは値ではなくセル(名札の場所)。呼ばれたときに開ける:遅延束縛。

  • ループで作った関数たちはセル一つを共有する。直し方:lambda i=i: i、functools.partial。

  • nonlocal でセルの中のタグを付け替えると、状態を持つ関数になる。状態が多ければクラス。

  • デコレーターは関数を受け取って関数を返すクロージャ。functools.wraps を忘れずに。

← 前へ

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

次へ →

7限目 · かばんの規格書:クラスとインスタンス

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

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

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

質問と回答

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

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

R

ReflexKR

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

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

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

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

Built with Reflex