見出し画像

Python 3: Deep Dive (Part 1 - Functional): クロージャとデコレータ (セクション7-4/11)

  • クロージャは、外側のスコープから変数を保持し、関数の状態を保存するために使われます。

  • デコレータは関数に追加の機能を付与し、`@`記号や`functools.wraps`で簡潔に使用できます。

  • パラメータ付きデコレータで柔軟性を持たせ、効率的で保守性の高いコードが可能です。

Pythonの関数型プログラミングの機能を学ぶ旅の中で、今回は非常に強力な概念であるクロージャデコレータにたどり着きます。これらのツールは、動作をカプセル化したり関数を動的に拡張することで、より簡潔で読みやすく、保守しやすいコードを書くことを可能にします。このブログ記事では、Udemyコース「Python 3: Deep Dive (Part 1 - Functional)」の第7セクション、レッスン104から107までを基に、クロージャの実用的な応用と、それがデコレータへと進化していく様子を詳しく探っていきます。


クロージャの導入

クロージャは、Pythonにおける関数オブジェクトの一種で、外側のスコープから変数へのアクセスを保持する機能です。外側の関数の実行が終了した後でも、クロージャはそのスコープにある変数にアクセスできます。これにより、グローバル変数やオブジェクトの属性を使わずに、関数呼び出し間で状態を保持することが可能です。


クロージャの応用 - パート1

アベレージャーの構築

まず、数値を次々に受け取り、その平均を計算するアベレージャーを構築してみましょう。従来の方法では、以下のようにクラスを使用します。

class Averager:
    def __init__(self):
        self.numbers = []
    
    def add(self, number):
        self.numbers.append(number)
        total = sum(self.numbers)
        count = len(self.numbers)
        return total / count

# 使用例
a = Averager()
print(a.add(10))  # 出力: 10.0
print(a.add(20))  # 出力: 15.0
print(a.add(30))  # 出力: 20.0

この方法は動作しますが、毎回合計とカウントを再計算するため、データが大きい場合には非効率的です。

効率性の向上

リストにすべての数値を保持するのではなく、現在の合計とカウントを維持することで効率化できます。

class Averager:
    def __init__(self):
        self._total = 0
        self._count = 0
    
    def add(self, value):
        self._total += value
        self._count += 1
        return self._total / self._count

# 使用例
a = Averager()
print(a.add(10))  # 出力: 10.0
print(a.add(20))  # 出力: 15.0
print(a.add(30))  # 出力: 20.0

クラスからクロージャへの置き換え

この機能をクロージャを使用して実装することで、コードを簡潔にすることができます。

def averager():
    total = 0
    count = 0
    
    def add(value):
        nonlocal total, count
        total += value
        count += 1
        return total / count
    
    return add

# 使用例
a = averager()
print(a(10))  # 出力: 10.0
print(a(20))  # 出力: 15.0
print(a(30))  # 出力: 20.0

ここで、`total`と`count`は、`add`クロージャによってキャプチャされた自由変数です。`nonlocal`キーワードを使用することで、内部関数から外側のスコープの変数を変更することが可能です。


クロージャの応用 - パート2

カウンタークロージャの作成

次に、呼び出されるたびに値を増加させるカウンタークロージャを作成してみましょう。

def counter(initial_value=0):
    value = initial_value
    
    def increment(step=1):
        nonlocal value
        value += step
        return value
    
    return increment

# 使用例
c = counter()
print(c())       # 出力: 1
print(c())       # 出力: 2
print(c(5))      # 出力: 7

関数呼び出しのカウント

このアイデアを拡張して、関数が呼び出された回数を数えるクロージャを作成できます。

def counter(fn):
    count = 0
    
    def inner(*args, **kwargs):
        nonlocal count
        count += 1
        print(f'{fn.__name__}{count}回呼び出されました')
        return fn(*args, **kwargs)
    
    return inner

# 使用例
@counter
def add(a, b):
    return a + b

print(add(2, 3))  # 出力: addが1回呼び出されました \n 5
print(add(5, 7))  # 出力: addが2回呼び出されました \n 12

ここでは、`add`関数をデコレートして、呼び出し回数をカウントする機能を追加しています。

複数の関数間で状態を共有

複数の関数に対してカウントを共有する場合、共有の辞書を利用します。

def counter(fn, counters):
    count = 0
    
    def inner(*args, **kwargs):
        nonlocal count
        count += 1
        counters[fn.__name__] = count
        return fn(*args, **kwargs)
    
    return inner

# 共有カウンター辞書
func_counters = {}

# 使用例
@counter(func_counters)
def multiply(a, b):
    return a * b

multiply(2, 3)
multiply(5, 7)
print(func_counters)  # 出力: {'multiply': 2}

`func_counters`を引数として渡すことで、グローバル変数に頼らず、コードをよりモジュール化できます。


デコレータの導入

デコレータの理解

デコレータは、他の関数を引数として取り、それに対して何らかの機能を追加し、元の関数または別の関数として返すものです。デコレータを使用すると、関数を他の関数で修飾し、拡張することができます。

`@`記号の使い方

Pythonではデコレータを使用する際に`@`記号を用いることで、よりシンプルな構文を提供しています。

@decorator
def function_to_decorate():
    pass

これは次と同等です:

def function_to_decorate():


    pass

function_to_decorate = decorator(function_to_decorate)

関数メタデータの保持

デコレータは関数のメタデータ(名前やドキュメンテーション文字列など)を変更する可能性があるため、`functools.wraps`を使ってそれらを保持することが重要です。


デコレータの実践

`functools.wraps`の使用

`functools.wraps`を使用する例を示します。

from functools import wraps

def counter(fn):
    count = 0
    
    @wraps(fn)
    def inner(*args, **kwargs):
        nonlocal count
        count += 1
        print(f'{fn.__name__}{count}回呼び出されました')
        return fn(*args, **kwargs)
    
    return inner

# 使用例
@counter
def factorial(n):
    """nの階乗を計算します。"""
    result = 1
    for i in range(2, n+1):
        result *= i
    return result

print(factorial(5))          # 出力: factorialが1回呼び出されました \n 120
print(factorial.__name__)    # 出力: factorial
print(factorial.__doc__)     # 出力: nの階乗を計算します。

`@wraps`を使わなければ、`factorial.name`は `'inner'` になり、ドキュメンテーションも失われてしまいます。

パラメータ付きデコレータ

デコレータは、引数を取るようにすることも可能です。この場合、もう一段階のネスティングを追加します。

from functools import wraps

def repeat(times):
    def decorator(fn):
        @wraps(fn)
        def inner(*args, **kwargs):
            result = None
            for _ in range(times):
                result = fn(*args, **kwargs)
            return result
        return inner
    return decorator

# 使用例
@repeat(3)
def greet(name):
    print(f'こんにちは、{name}さん!')

greet('アリス')
# 出力:
# こんにちは、アリスさん!
# こんにちは、アリスさん!
# こんにちは、アリスさん!

ここで、`repeat`はパラメータ`times`にアクセスできるデコレータファクトリです。


まとめ

クロージャとデコレータは、Pythonで非常に強力なツールであり、コードの再利用性と設計の明確さを向上させます。クロージャは、状態を保持しながら柔軟な関数を実現し、デコレータは関数やメソッドを修飾するためのクリーンで表現力豊かな方法を提供します。

主なポイント:

  • クロージャは、そのスコープ外から変数をキャプチャすることで、状態を保持します。

  • デコレータは、他の関数を変更するための関数であり、多くの場合、クロージャとして実装されます。

  • Pythonの`@`記号は、デコレータの使用を簡潔にします。

  • 関数メタデータは`functools.wraps`を使うことで保持できます。

  • パラメータ付きデコレータは、もう一段階の関数ネスティングを導入することで引数を受け取ります。

これらの概念をマスターすることで、より表現力豊かで効率的、かつ保守性の高いPythonコードを書くことが可能になります。これらは、高度なプログラミングパターンへの扉を開き、関数の能力を大幅に強化します。


次のレッスンでは、さらに高度なデコレータの使用方法、クラスデコレータ、および実際のシナリオでの実用例について探求しますので、お楽しみに!

「超本当にドラゴン」へ

いいなと思ったら応援しよう!