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キーワードを使うことで外部変数の変更も可能になります。