Pythonにおけるクロージャの仕組みと実用例

Pythonでは、関数内で別の関数を定義し、それを返却することでクロージャを実現できます。これは、関数型プログラミングの基本概念の一つであり、「内部関数が外部関数のスコープにある変数を参照・保持する仕組み」を指します。

以下は、最も単純なクロージャの例です:

def outer():
    def inner():
        return "hello"
    return inner

f = outer()
print(f())  # 出力: hello

この例では、outer()が呼び出された後、そのローカルスコープは終了しているはずですが、返されたinner関数は依然として"hello"を返せます。これは、innerがouterのスコープに依存しているためではありません(この例では変数を参照していないため)、単に返却された関数オブジェクトとして機能しているだけです。しかし、段々と実際のクロージャの意味が明らかになります。

外側のスコープの変数を保持する例

クロージャの真価は、外部関数の変数(=自由変数)を内部関数が参照・保持するときに発揮されます:

def make_counter():
    count = 0  # 自由変数

    def increment(step=1):
        nonlocal count
        count += step
        return count

    return increment

counter = make_counter()
print(counter())     # 1
print(counter())     # 2
print(counter(3))    # 5

ここでは、countはincrementの外側(親スコープ)で定義された変数であり、nonlocal宣言により「この変数はローカルではなく親スコープにある」と明示しています。

通常の関数では、関数呼び出しが終了するとそのローカル変数はガベージコレクションの対象になりますが、クロージャでは内部関数が存続している限り、外部関数の変数も保持されます。この特性はデータの隐藏(カプセル化)に役立ちますが、それと引き換えにメモリ使用量が増加する可能性があります。

グローバル変数と比較した場合の利点

例えば、預金残高を管理するシミュレーションを考えます。以下はグローバル変数を利用した実装です:

balance = 100

def deposit(amount):
    global balance
    if amount > 0:
        balance += amount
        print(f"入金 {amount} → 残高: {balance}")

def withdraw(amount):
    global balance
    if 0 < amount <= balance:
        balance -= amount
        print(f"出金 {amount} → 残高: {balance}")
    else:
        print("残高不足または無効な入力")

deposit(50)
withdraw(30)
balance = -9999  # 任意のコードで不正変更可能!
withdraw(10)     # 今度は残高不足

この実装では、balanceがグローバルスコープにあり、どこからでも書き換え可能であるため、意図せぬ値の破壊が発生しやすくなります。

一方、クロージャを用いた実装では、自由変数としてのbalanceは外側から直接アクセスできません:

def create_account(initial=0):
    balance = initial

    def deposit(amount):
        nonlocal balance
        if amount <= 0:
            raise ValueError("入金額は正でなければなりません")
        balance += amount
        return balance

    def withdraw(amount):
        nonlocal balance
        if amount <= 0:
            raise ValueError("出金額は正でなければなりません")
        if amount > balance:
            raise ValueError("残高不足です")
        balance -= amount
        return balance

    return deposit, withdraw

deposit_fn, withdraw_fn = create_account(100)
print(deposit_fn(20))   # 120
print(withdraw_fn(30))  # 90

# 直接変数にアクセスできないため、不正な操作は不可能
# print(create_account.balance)  # AttributeError

このように、クロージャを利用することで、データのカプセル化と状態の追跡を実現できます。

クラスとの比較

Pythonでは、上記のような用途にクラスやインスタンス変数を用いることが一般的です。クロージャはマスプログラミングやシンプルな状態管理に適していますが、複雑なロジックや継承・ポリモーフィズムが必要라면クラスベースの実装の方が理解・保守しやすい傾向があります。

デコレータとしてのクロージャ

また、Pythonのデコレータは本質的にクロージャの応用であり、関数の前後に処理を差し込むために広く利用されています:

def log_calls(func):
    def wrapper(*args, **kwargs):
        print(f"[LOG] 呼び出し: {func.__name__} with {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"[LOG] 戻り値: {result}")
        return result
    return wrapper

@log_calls
def multiply(a, b):
    return a * b

multiply(3, 4)
# [LOG] 呼び出し: multiply with (3, 4), {}
# [LOG] 戻り値: 12

このwrapperは、クロージャとしてfuncを捕捉し、挙動を拡張しています。

タグ: closure nonlocal scope functions encapsulation

5月26日 10:29 投稿