Pythonの基礎 - クロージャ
概要
クロージャとは、参照環境を伴った関数、あるいはその関数への参照のことを指す。
クロージャは、関数内関数やC / C++ / C#の関数オブジェクトに似ている。
関数内の変数を扱う時、その関数が宣言された時のスコープによって実行されるというような説明もできる。
クロージャは、外側の関数の変数を「記憶」して、後から実行することができる強力な機能である。
主な用途:
- 状態の保持 (カウンタ等)
- 関数ファクトリ (設定値を持つ関数の生成)
- デコレータの実装
- コールバック関数のカスタマイズ
関数内関数とクロージャの違い
まず、クロージャを理解するために、関数内関数を見ておく。
# 関数内関数
def OuterFunc(a, b):
def InnerFunc():
return a + b
return InnerFunc()
print(OuterFunc(2, 3))
# 実行例 : 5
クロージャは、上記のサンプルコードを次のように変更する。
以下の例では、OuterFunc関数の戻り値が、InnerFunc関数を呼び出すのではなく、丸括弧を使用せずにInnerFuncオブジェクトを記述している。
このサンプルコードを実行すると、InnerFuncオブジェクトのアドレスが返される。
これは、まだInnerFunc関数が実行されていない状態である。
この状態を記憶して、後で使用することができるものがクロージャである。
ここでは、InnerFuncがクロージャになる。
def OuterFunc(a, b):
def InnerFunc():
return a + b
return InnerFunc
print(OuterFunc(2, 3))
# 実行例 : <function outer_function.<locals>.inner_function at 0x10d86b7b8>
クロージャの実行
クロージャを実行するには、次のように行う。
以下の例では、OuterFunc関数をfuncオブジェクトに代入している。
これに、丸括弧()を付けてfuncオブジェクトを実行している。
def OuterFunc(a, b):
def InnerFunc():
return a + b
return InnerFunc
func = OuterFunc(2, 3)
print(func())
# 実行例 : 5
次に、長方形の面積を計算するサンプルコードを記述する。
以下の例では、引数に横(width)を与えるAreaCalc関数を定義して、その中に縦(height)を引数を定義して面積を計算するAreaCalcFunc関数を定義している。
戻り値は、丸括弧()を外したAreaCalcFuncオブジェクトを返す。(クロージャになっている)
まず、2種類の横の数値を与えた後、オブジェクト変数ac1とac2にAreaCalcFuncオブジェクトを代入している。
このオブジェクト変数ac1とac2に丸括弧()を付けて縦の数値を与えることで計算が実行される。
def AreaCalc(width):
def AreaCalcFunc(height):
return width * height
return AreaCalcFunc
# widthが25と50の場合を計算
ac1 = AreaCalc(25)
ac2 = AreaCalc(50)
# heightを10として面積を求める
print(ac1(10))
print(ac2(10))
# 実行例 : 250 500
クロージャの仕組み (自由変数とスコープ)
クロージャを理解するには、Pythonの スコープルール (LEGBルール) と 自由変数 の概念を知る必要がある。
LEGBルール
Pythonは変数を以下の順序で探索する。
- L (Local)
- ローカルスコープ (関数内)
- E (Enclosing)
- 外側の関数のスコープ
- G (Global)
- グローバルスコープ (モジュールレベル)
- B (Built-in)
- 組み込みスコープ (len, print等)
束縛変数と自由変数
- 束縛変数
- 関数内で定義された変数
- 自由変数
- 関数内で使用されているが、その関数内で定義されていない変数 (外側のスコープから取得)
以下の例では、InnerFunc内のxは自由変数である。
def OuterFunc(x):
# xはOuterFuncの束縛変数
def InnerFunc(y):
# yはInnerFuncの束縛変数
# xはInnerFuncの自由変数 (外側のスコープから取得)
return x + y
return InnerFunc
closure = OuterFunc(10)
print(closure(5))
# 実行例 : 15
クロージャは、自由変数の値を関数オブジェクト内に保持する。
OuterFunc関数の実行が終わった後も、InnerFunc関数はxの値 (10) を記憶している。
nonlocalキーワード
クロージャで外側のスコープの変数を変更するには、nonlocal キーワードを使用する。
参照と代入の違い
外側のスコープの変数を参照するだけなら、nonlocalは不要である。
しかし、代入を行うと、新しいローカル変数が作成されてしまう。
def Counter():
count = 0
def Increment():
# これはエラーになる (countに代入する前に参照している)
count = count + 1
return count
return Increment
# UnboundLocalError: local variable 'count' referenced before assignment
nonlocalを使用した正しい実装
nonlocal キーワードを使用すると、外側のスコープの変数を変更できる。
def Counter():
count = 0
def Increment():
nonlocal count
count = count + 1
return count
return Increment
counter = Counter()
print(counter())
print(counter())
print(counter())
# 実行例 : 1 2 3
以下の例では、nonlocalを使用して複数の操作を提供するカウンタを実装している。
def CounterWithReset():
count = 0
def Increment():
nonlocal count
count += 1
return count
def Decrement():
nonlocal count
count -= 1
return count
def Reset():
nonlocal count
count = 0
return count
def GetValue():
return count
return Increment, Decrement, Reset, GetValue
inc, dec, reset, get = CounterWithReset()
print(inc())
print(inc())
print(dec())
print(get())
print(reset())
# 実行例 : 1 2 1 1 0
クロージャの実践的な使用例
クロージャは、状態を保持する関数や、特定の設定値を持つ関数を生成する時に便利である。
カウンター (状態保持)
以下の例では、カウンターの状態をクロージャで保持している。
def MakeCounter():
count = 0
def Counter():
nonlocal count
count += 1
return count
return Counter
counter1 = MakeCounter()
counter2 = MakeCounter()
print(counter1())
print(counter1())
print(counter2())
print(counter1())
# 実行例 : 1 2 1 3
関数ファクトリ (乗算器)
以下の例では、特定の係数で乗算する関数を生成している。
def MakeMultiplier(factor):
def Multiply(x):
return x * factor
return Multiply
times2 = MakeMultiplier(2)
times5 = MakeMultiplier(5)
times10 = MakeMultiplier(10)
print(times2(3))
print(times5(3))
print(times10(3))
# 実行例 : 6 15 30
設定値のラッピング
以下の例では、デフォルト設定値を持つ関数を生成している。
def MakeGreeter(greeting):
def Greet(name):
return f"{greeting}, {name}!"
return Greet
english_greeter = MakeGreeter("Hello")
japanese_greeter = MakeGreeter("こんにちは")
french_greeter = MakeGreeter("Bonjour")
print(english_greeter("Alice"))
print(japanese_greeter("太郎"))
print(french_greeter("Marie"))
# 実行例 : Hello, Alice! こんにちは, 太郎! Bonjour, Marie!
クロージャとデコレータ
デコレータは、クロージャの代表的な応用例である。
デコレータは、関数を受け取って、その関数を拡張した新しい関数を返す。
基本的なデコレータ
以下の例では、関数の実行時間を計測するデコレータを実装している。
import time
def TimerDecorator(func):
def Wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__}の実行時間: {end - start:.4f}秒")
return result
return Wrapper
@TimerDecorator
def SlowFunction():
time.sleep(1)
return "完了"
result = SlowFunction()
print(result)
# 実行例 : SlowFunctionの実行時間: 1.0012秒 完了
functools.wrapsの重要性
デコレータを実装する場合、functools.wrapsを使用して元の関数のメタデータを保持する。
from functools import wraps
import time
def TimerDecorator(func):
@wraps(func)
def Wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__}の実行時間: {end - start:.4f}秒")
return result
return Wrapper
@TimerDecorator
def Calculate(x, y):
"""2つの数値を加算する"""
return x + y
# functools.wrapsを使用すると、元の関数名とドキュメントが保持される
print(Calculate.__name__)
print(Calculate.__doc__)
print(Calculate(3, 5))
# 実行例 : Calculate 2つの数値を加算する Calculateの実行時間: 0.0000秒 8
引数付きデコレータ
以下の例では、引数を受け取るデコレータを定義している。
def Repeat(times):
def Decorator(func):
def Wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return Wrapper
return Decorator
@Repeat(3)
def Greet(name):
print(f"Hello, {name}!")
Greet("Alice")
# 実行例 : Hello, Alice! Hello, Alice! Hello, Alice!
クロージャを使用する時の注意
ループ内でのクロージャ問題
ループ内でクロージャを作成すると、予期しない動作をする場合がある。
以下の例では、クロージャがiの参照を保持しているため、ループ終了時のiの値 (2) が使用されてしまう。
# 間違った実装
functions = []
for i in range(3):
def func():
return i
functions.append(func)
# すべて2を返す (期待: 0, 1, 2)
for f in functions:
print(f())
# 実行例 : 2 2 2
解決方法 1 : デフォルト引数を使用する
デフォルト引数を使用すると、ループの各反復時の値をキャプチャできる。
functions = []
for i in range(3):
def func(x=i):
return x
functions.append(func)
for f in functions:
print(f())
# 実行例 : 0 1 2
解決方法 2 : 関数ファクトリを使用する
外側の関数でパラメータをキャプチャする。
def MakeFunc(i):
def func():
return i
return func
functions = []
for i in range(3):
functions.append(MakeFunc(i))
for f in functions:
print(f())
# 実行例 : 0 1 2
解決方法 3 : ラムダ式とデフォルト引数
ラムダ式でもデフォルト引数を使用できる。
functions = [lambda x=i: x for i in range(3)]
for f in functions:
print(f())
# 実行例 : 0 1 2
クロージャとラムダ式
ラムダ式でもクロージャを形成できる。
ラムダ式によるクロージャ
以下の例では、ラムダ式を使用してクロージャを作成している。
def MakeAdder(x):
return lambda y: x + y
add5 = MakeAdder(5)
add10 = MakeAdder(10)
print(add5(3))
print(add10(3))
# 実行例 : 8 13
ラムダ式とdefの比較
以下の2つの実装は同じ動作をする。
# def文を使用
def MakeMultiplier_def(factor):
def Multiply(x):
return x * factor
return Multiply
# ラムダ式を使用
def MakeMultiplier_lambda(factor):
return lambda x: x * factor
times3_def = MakeMultiplier_def(3)
times3_lambda = MakeMultiplier_lambda(3)
print(times3_def(7))
print(times3_lambda(7))
# 実行例 : 21 21
ラムダ式は簡潔だが、複雑なロジックにはdef文を使用する方が読みやすい。
__closure__属性
クロージャの内部構造は、__closure__ 属性を使用して検査できる。
クロージャの内部を確認する
以下の例では、クロージャが保持している自由変数を確認している。
def OuterFunc(x, y):
def InnerFunc(z):
return x + y + z
return InnerFunc
closure = OuterFunc(10, 20)
# クロージャの情報を確認
print(f"クロージャ: {closure.__closure__}")
print(f"セルの数: {len(closure.__closure__)}")
# 各セルの値を確認
for i, cell in enumerate(closure.__closure__):
print(f"セル{i}: {cell.cell_contents}")
print(f"実行結果: {closure(5)}")
# 実行例 : クロージャ: (<cell at 0x...: int object at 0x...>, <cell at 0x...: int object at 0x...>) セルの数: 2 セル0: 10 セル1: 20 実行結果: 35
クロージャでない関数
自由変数を持たない関数は、__closure__ が None になる。
def SimpleFunc(x):
return x * 2
print(f"クロージャ: {SimpleFunc.__closure__}")
# 実行例 : クロージャ: None
クロージャとクラスの比較
クロージャとクラスは、どちらも状態を保持できる。
どちらを使用するかは、用途によって決める。
クロージャ版のカウンタ
def MakeCounter():
count = 0
def Increment():
nonlocal count
count += 1
return count
def GetValue():
return count
return Increment, GetValue
increment, get_value = MakeCounter()
print(increment())
print(increment())
print(get_value())
# 実行例 : 1 2 2
クラス版のカウンタ
class Counter:
def __init__(self):
self.count = 0
def Increment(self):
self.count += 1
return self.count
def GetValue(self):
return self.count
counter = Counter()
print(counter.Increment())
print(counter.Increment())
print(counter.GetValue())
# 実行例 : 1 2 2
クロージャとクラスの使い分け
- クロージャを使用すべき場合
- 単純な状態保持 (1〜2個の変数)
- 関数ファクトリ (特定の設定値を持つ関数の生成)
- デコレータの実装
- 一時的なコールバック関数
- クラスを使用すべき場合
- 複数の状態変数と複数のメソッド
- 継承が必要な場合
- 明確なインターフェースが必要な場合
- 状態の可視性が重要な場合 (self.count等)