Documentation Python

Pythonのクロージャ(closure)は、外部関数のスコープにある変数を参照・保持する内部関数です。関数が「状態」を持つことができ、オブジェクト指向の代替手法として活用できます。

クロージャ vs クラス vs グローバル変数

手法状態の保持カプセル化複数インスタンス複雑さ
クロージャ
クラス
グローバル変数××低(危険)

クロージャの基本

構造

def outer_function(param):  # 外部関数
    # 外部スコープの変数(自由変数)
    free_variable = param

    def inner_function(arg):  # 内部関数
        # 自由変数を参照
        return free_variable + arg

    return inner_function  # 内部関数を返す

基本例

def make_adder(x):
    """xを加算するクロージャを生成"""
    def adder(y):
        return x + y
    return adder

# クロージャを生成
add_5 = make_adder(5)
add_10 = make_adder(10)

# 各クロージャは独立した状態を持つ
print(add_5(3))   # 8
print(add_10(3))  # 13

# クロージャの自由変数を確認
print(add_5.__closure__[0].cell_contents)  # 5

クロージャの条件

def is_closure_example():
    """クロージャの3つの条件"""
    # 1. ネストした関数が存在する
    # 2. 内部関数が外部関数の変数を参照する
    # 3. 外部関数が内部関数を返す

    x = 10  # 外部変数

    def inner():
        return x  # 外部変数を参照

    return inner  # 内部関数を返す

closure = is_closure_example()
print(closure())  # 10
print(closure.__closure__)  # (<cell at ...: int object at ...>,)

nonlocalキーワード

変数の参照と変更

def counter():
    """カウンターのクロージャ"""
    count = 0

    def increment():
        nonlocal count  # 外部変数を変更可能にする
        count += 1
        return count

    return increment

# 使用例
counter1 = counter()
counter2 = counter()  # 独立したカウンター

print(counter1())  # 1
print(counter1())  # 2
print(counter1())  # 3

print(counter2())  # 1(独立している)

nonlocalなしの場合

def broken_counter():
    count = 0

    def increment():
        # nonlocalがないとエラー
        # count += 1  # UnboundLocalError
        return count  # 参照のみなら可能

    return increment

# 正しい方法
def working_counter():
    count = [0]  # ミュータブルなオブジェクトを使う

    def increment():
        count[0] += 1  # リストの要素は変更可能
        return count[0]

    return increment

実践的なパターン

関数ファクトリ

def make_multiplier(factor):
    """乗算器を生成するファクトリ"""
    def multiplier(x):
        return x * factor
    return multiplier

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))  # 10
print(triple(5))  # 15

# リスト内包表記で複数生成
multipliers = [make_multiplier(i) for i in range(1, 6)]
print([m(10) for m in multipliers])  # [10, 20, 30, 40, 50]

設定を保持するクロージャ

def create_formatter(prefix="", suffix="", uppercase=False):
    """文字列フォーマッタを生成"""
    def formatter(text):
        result = text
        if uppercase:
            result = result.upper()
        return f"{prefix}{result}{suffix}"
    return formatter

# 使用例
bold = create_formatter("<b>", "</b>")
shout = create_formatter(">>> ", " <<<", uppercase=True)
quote = create_formatter('"', '"')

print(bold("Hello"))    # <b>Hello</b>
print(shout("Hello"))   # >>> HELLO <<<
print(quote("Hello"))   # "Hello"

キャッシュ(メモ化)

def memoize(func):
    """関数の結果をキャッシュするクロージャ"""
    cache = {}

    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]

    # キャッシュにアクセスできるようにする
    wrapper.cache = cache
    wrapper.clear_cache = lambda: cache.clear()

    return wrapper

@memoize
def fibonacci(n):
    """フィボナッチ数列(再帰)"""
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# メモ化により高速化
print(fibonacci(100))  # 354224848179261915075
print(fibonacci.cache)  # キャッシュの中身を確認

カウンター with リセット機能

def make_counter(start=0, step=1):
    """リセット可能なカウンター"""
    state = {'count': start, 'step': step}

    def counter():
        current = state['count']
        state['count'] += state['step']
        return current

    def reset():
        state['count'] = start

    def set_step(new_step):
        state['step'] = new_step

    def get_count():
        return state['count']

    # 追加の関数を属性として付加
    counter.reset = reset
    counter.set_step = set_step
    counter.get_count = get_count

    return counter

# 使用例
c = make_counter(0, 1)
print(c())  # 0
print(c())  # 1
print(c())  # 2

c.set_step(5)
print(c())  # 3
print(c())  # 8

c.reset()
print(c())  # 0

ロガー

from datetime import datetime

def create_logger(name, level="INFO"):
    """名前付きロガーを生成"""
    levels = {"DEBUG": 0, "INFO": 1, "WARNING": 2, "ERROR": 3}
    min_level = levels.get(level, 1)

    def log(message, msg_level="INFO"):
        if levels.get(msg_level, 1) >= min_level:
            timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            print(f"[{timestamp}] [{msg_level}] [{name}] {message}")

    def debug(message):
        log(message, "DEBUG")

    def info(message):
        log(message, "INFO")

    def warning(message):
        log(message, "WARNING")

    def error(message):
        log(message, "ERROR")

    log.debug = debug
    log.info = info
    log.warning = warning
    log.error = error

    return log

# 使用例
app_logger = create_logger("App", "DEBUG")
db_logger = create_logger("Database", "WARNING")

app_logger.debug("デバッグメッセージ")
app_logger.info("情報メッセージ")
db_logger.warning("警告メッセージ")

遅延評価

def lazy(func):
    """遅延評価を実現するクロージャ"""
    result = []  # ミュータブルで状態を保持
    computed = [False]

    def wrapper():
        if not computed[0]:
            result.append(func())
            computed[0] = True
        return result[0]

    return wrapper

# 使用例
@lazy
def expensive_computation():
    print("計算実行中...")
    return sum(range(1000000))

# 最初の呼び出しで計算
print(expensive_computation())  # 計算実行中... 499999500000

# 2回目以降はキャッシュを使用
print(expensive_computation())  # 499999500000(計算しない)

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

基本的なデコレータ

def timing(func):
    """実行時間を計測するデコレータ"""
    import time

    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__}: {end - start:.4f}秒")
        return result

    return wrapper

@timing
def slow_function():
    import time
    time.sleep(1)
    return "完了"

slow_function()  # slow_function: 1.0012秒

引数付きデコレータ

def retry(max_attempts=3, delay=1):
    """リトライ機能を追加するデコレータ"""
    import time

    def decorator(func):
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    print(f"Attempt {attempt + 1} failed: {e}")
                    if attempt < max_attempts - 1:
                        time.sleep(delay)
            raise last_exception
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5)
def unreliable_api():
    import random
    if random.random() < 0.7:
        raise ConnectionError("API接続失敗")
    return "成功"

検証デコレータ

def validate_args(*validators):
    """引数を検証するデコレータ"""
    def decorator(func):
        def wrapper(*args, **kwargs):
            for i, (arg, validator) in enumerate(zip(args, validators)):
                if not validator(arg):
                    raise ValueError(f"引数{i}の検証に失敗: {arg}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

# バリデータ関数
is_positive = lambda x: x > 0
is_string = lambda x: isinstance(x, str)
is_not_empty = lambda x: len(x) > 0

@validate_args(is_positive, is_string)
def create_user(age, name):
    return {"age": age, "name": name}

print(create_user(25, "Alice"))  # {'age': 25, 'name': 'Alice'}
# create_user(-1, "Bob")  # ValueError: 引数0の検証に失敗: -1

ループ内でのクロージャの注意点

よくある間違い

# 間違った例
def create_functions_wrong():
    functions = []
    for i in range(5):
        def func():
            return i  # iは最後の値(4)を参照
        functions.append(func)
    return functions

funcs = create_functions_wrong()
print([f() for f in funcs])  # [4, 4, 4, 4, 4] - すべて4

# 正しい例1: デフォルト引数を使用
def create_functions_correct1():
    functions = []
    for i in range(5):
        def func(x=i):  # デフォルト引数で値をキャプチャ
            return x
        functions.append(func)
    return functions

funcs = create_functions_correct1()
print([f() for f in funcs])  # [0, 1, 2, 3, 4]

# 正しい例2: ファクトリ関数を使用
def create_functions_correct2():
    def make_func(x):
        def func():
            return x
        return func

    return [make_func(i) for i in range(5)]

funcs = create_functions_correct2()
print([f() for f in funcs])  # [0, 1, 2, 3, 4]

クロージャ vs クラス

# クロージャ版
def make_bank_account(initial_balance):
    """銀行口座をクロージャで実装"""
    balance = initial_balance

    def deposit(amount):
        nonlocal balance
        balance += amount
        return balance

    def withdraw(amount):
        nonlocal balance
        if amount > balance:
            raise ValueError("残高不足")
        balance -= amount
        return balance

    def get_balance():
        return balance

    return {
        'deposit': deposit,
        'withdraw': withdraw,
        'get_balance': get_balance
    }

# クラス版
class BankAccount:
    """銀行口座をクラスで実装"""
    def __init__(self, initial_balance):
        self._balance = initial_balance

    def deposit(self, amount):
        self._balance += amount
        return self._balance

    def withdraw(self, amount):
        if amount > self._balance:
            raise ValueError("残高不足")
        self._balance -= amount
        return self._balance

    def get_balance(self):
        return self._balance

# 使用例(どちらも同じように使える)
account1 = make_bank_account(1000)
print(account1['deposit'](500))     # 1500
print(account1['withdraw'](200))    # 1300

account2 = BankAccount(1000)
print(account2.deposit(500))        # 1500
print(account2.withdraw(200))       # 1300

__closure__属性

def outer(x, y):
    def inner():
        return x + y
    return inner

closure = outer(10, 20)

# クロージャの情報を確認
print(closure.__closure__)  # タプルでセルオブジェクトを保持
print(len(closure.__closure__))  # 2

# 各セルの値を確認
for i, cell in enumerate(closure.__closure__):
    print(f"cell[{i}]: {cell.cell_contents}")
# cell[0]: 10
# cell[1]: 20

# 自由変数の名前
print(closure.__code__.co_freevars)  # ('x', 'y')

まとめ

パターン用途
関数ファクトリ設定済みの関数を生成
カウンター状態を保持するカウント
メモ化計算結果のキャッシュ
ロガー設定を保持したロギング
デコレータ関数の機能拡張
遅延評価必要時まで計算を遅延

クロージャは、関数が状態を持つことを可能にする強力な仕組みです。シンプルなケースではクラスの代替として使え、デコレータの実装やコールバック関数の作成に欠かせません。nonlocalキーワードを使うことで外部変数の変更も可能になります。

参考文献

円