Pythonの基礎 - クロージャ

提供: MochiuWiki : SUSE, EC, PCB

概要

クロージャとは、参照環境を伴った関数、あるいはその関数への参照のことを指す。
クロージャは、関数内関数や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等)