Python | 関数:クロージャ

Python
スポンサーリンク

概要(クロージャは「外側の値を覚えた関数」をつくる仕組み)

クロージャは、関数の中で定義した内側関数が「外側の関数の変数」を覚えたまま動く仕組みです。外側関数の実行が終わっても、その値を保持し続けます。これにより「設定を閉じ込める」「状態を安全に持つ」など、シンプルで強力な小さな API を作れます。

def make_adder(step):
    def add(x):
        return x + step   # 外側の step を覚えている
    return add

plus10 = make_adder(10)
print(plus10(5))  # 15
Python

基本構文と動き(ここが重要)

外側関数・内側関数・返す流れ

クロージャは「外側で値を受ける → 内側でその値を使う → 内側関数を返す」という流れで作ります。返された内側関数を、後から何度でも呼べます。

def make_rounder(ndigits):
    def rounder(x):
        return round(x, ndigits)
    return rounder

r2 = make_rounder(2)
print(r2(3.14159))  # 3.14
Python

スコープと参照(nonlocal の位置づけ)

内側関数は外側の「ローカル変数」を参照できます。もしその外側変数を「更新」したいなら nonlocal を使います。参照だけなら nonlocal は不要です。

def make_counter(start=0):
    n = start
    def inc():
        nonlocal n
        n += 1
        return n
    return inc

c = make_counter()
print(c())  # 1
print(c())  # 2
Python

クロージャの強み(設定固定・状態保持・カプセル化)を深掘り

設定を固定して「短い呼び出し」にする

毎回同じパラメータを渡す代わりに、外側で一度だけ受けて閉じ込めます。以降の呼び出しは本質的な入力だけ。

def make_formatter(prefix="", suffix=""):
    def fmt(x):
        return f"{prefix}{x}{suffix}"
    return fmt

info = make_formatter("[", "]")
print(info("OK"))  # [OK]
Python

状態を安全に持つ(グローバル変数の代替)

グローバル変数を書き換えるとバグの温床になります。クロージャは「関数が自分の状態を持つ」ので、外部から勝手に触らせない設計ができます。

def memoize(func):
    cache = {}
    def wrapper(x):
        if x in cache:
            return cache[x]
        y = func(x)
        cache[x] = y
        return y
    return wrapper

@memoize
def slow_square(n):
    return n * n
Python

カプセル化(外へ余計な名前を公開しない)

外側の関数専用のヘルパー処理は、内側関数に閉じ込めます。モジュール全体に余計な関数名を増やさず、意図を明確にできます。

def normalize(text):
    def trim(s): return s.strip()
    def lower(s): return s.casefold()
    return lower(trim(text))
Python

使いどころと設計の勘所(読みやすさ・テスト・パフォーマンス)

いつクロージャを選ぶか(def・クラスとの比較)

  • 短い「設定固定」「小さな状態保持」ならクロージャが最短距離。
  • 複雑な状態・複数メソッドが必要ならクラスが向いています。
  • 公開関数で説明(docstring・型ヒント)が重要なら、トップレベルの def のほうが可読性が高いです。

テスト容易性のための形にする

「外側で設定 → 関数を返す」形は、その関数単体をユニットテストしやすいです。入力に対して決まった出力を返す設計に徹すると、テストが明快になります。

def make_threshold_filter(th):
    def ok(x):
        return x >= th
    return ok

is_adult = make_threshold_filter(20)
assert is_adult(18) is False
assert is_adult(25) is True
Python

定義コストとホットパス

外側関数を呼ぶたびに内側関数が定義されます。超高頻度の生成がボトルネックなら、事前に作って使い回す、またはトップレベルへ切り出す選択肢を検討しましょう。通常のスクリプトでは問題になりにくいです。

よくある落とし穴と対策(遅延束縛・nonlocalの濫用)

ループでの遅延束縛の罠(その時の値を固定する)

ループ内でラムダや内側関数を作ると「最後の値」を参照してしまうことがあります。デフォルト引数で束縛して、その時点の値を固定します。

funcs = []
for i in range(3):
    def f(x, i=i):   # i=i で今の i を閉じ込める
        return x + i
    funcs.append(f)

print([g(10) for g in funcs])  # [10, 11, 12]
Python

nonlocal の使いすぎは読みにくさに直結

状態更新が増えてくると追跡が難しくなります。更新が多いならクラスへ移行し、責務を分けて管理してください。クロージャは「薄い状態」に留めると美しく保てます。

例題で身につける(定番から一歩先まで)

例題1:加算器(設定固定)

def make_adder(step):
    def add(x):
        return x + step
    return add

add5 = make_adder(5)
print(add5(12))  # 17
Python

例題2:カウンタ(状態保持)

def make_counter(start=0):
    n = start
    def inc():
        nonlocal n
        n += 1
        return n
    return inc

c = make_counter(10)
print(c(), c(), c())  # 11 12 13
Python

例題3:検証関数ファクトリ(バリデーション)

def make_length_checker(min_len=1, max_len=100):
    def check(s):
        s = s.strip()
        return min_len <= len(s) <= max_len
    return check

ok = make_length_checker(3, 10)
print(ok("tea"))      # True
print(ok("   "))      # False
Python

例題4:軽いメモ化(キャッシュ)

def memoize(func):
    cache = {}
    def wrapper(x):
        if x in cache:
            return cache[x]
        y = func(x)
        cache[x] = y
        return y
    return wrapper

@memoize
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

print(fib(30))  # キャッシュありで高速化
Python

まとめ

クロージャは「外側の値を覚えた関数」を作る仕組みで、設定固定・小さな状態保持・カプセル化に抜群の効果を発揮します。参照だけならそのまま、更新が必要なら nonlocal。ループでは遅延束縛に注意して「デフォルト引数で束縛」する。複雑化したらクラスへ——この線引きを身につければ、あなたのコードは短く、意図が明快で、再利用しやすくなります。

タイトルとURLをコピーしました