見出し画像

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

  • デコレータの活用例として、関数の実行時間を計測する`@timed`とログを記録する`@logged`を実装し、異なる手法でFibonacci数を計算しました。

  • デコレータのスタック順序が重要であり、`@timed`と`@logged`の順序によって出力結果が異なることを確認しました。

  • スタックデコレータの応用として、APIの認証とログ記録の例が挙げられ、デコレータの順序がシステム動作に影響する点を強調しました。

Pythonのスコープ、クロージャ、デコレーターについての学習を進めてきましたが、いよいよこれらの概念を使って関数を実際に強化するエキサイティングな段階に入りました。セクション7のレッスン108と109では、デコレーターを使用して関数の実行時間を計測したり、関数呼び出しをログに記録する方法に焦点を当てています。このブログ記事では、デコレーターを使ってコードをよりクリーンで保守しやすくするための実用的な方法について解説します。


デコレーターの導入

デコレーターは、関数を変更せずにその動作を変更・強化できるPythonの強力な機能です。デコレーターは、別の関数を引数として受け取り、その動作を拡張して新しい関数を返します。これは、クロージャを通じて、内部関数が囲まれたスコープから変数にアクセスできるために可能となっています。

デコレーターの構文は、`@decorator_name`という表記を使うことで簡素化されています。これは、`function = decorator_name(function)`という書き方の糖衣構文(シンタックスシュガー)です。


タイマー・デコレーターの作成

デコレーターの一般的な用途の1つは、関数の実行時間を計測することです。これは、パフォーマンス分析や最適化に特に役立ちます。

タイマー・デコレーターの実装

まず、関数の実行時間を計測するためのデコレーター`timed`を作成しましょう。

def timed(fn):
    from time import perf_counter
    from functools import wraps

    @wraps(fn)
    def inner(*args, **kwargs):
        start = perf_counter()
        result = fn(*args, **kwargs)
        end = perf_counter()
        elapsed = end - start

        args_ = [repr(a) for a in args]
        kwargs_ = [f"{k}={v!r}" for k, v in kwargs.items()]
        all_args = args_ + kwargs_
        args_str = ', '.join(all_args)
        print(f"{fn.__name__}({args_str}) took {elapsed:.6f}s to run.")
        return result

    return inner

説明:

  • モジュールのインポート:

    • `perf_counter`は高精度のタイミング測定に使用されます。

    • `wraps`は元の関数のメタデータを保持します。

  • 内部関数の定義:

    • `*args`と`**kwargs`で任意の引数を受け取れるようにしています。

    • 関数`fn`を呼び出す前後でタイミングを測定します。

    • 経過時間を計算します。

  • 出力のフォーマット:

    • 引数を文字列に変換し、関数名、引数、実行時間を表示します。

  • 結果の返却:

    • 元の関数の結果を返します。

異なるFibonacci実装のタイミング測定

`timed`デコレーターを使って、Fibonacci数を求める3つの異なる実装を試してみます。

  1. 再帰的アプローチ

  2. ループアプローチ

  3. reduceを使った関数型プログラミング

Fibonacci数列は`1, 1, 2, 3, 5, 8, ...`で始まると仮定し、最初のFibonacci数をインデックス`1`とします。

再帰的アプローチ

再帰的な方法は簡潔ですが、重複計算が多いため効率が悪いです。

def fib_recursive(n):
    if n <= 2:
        return 1
    else:
        return fib_recursive(n-1) + fib_recursive(n-2)

`timed`デコレーターを適用します。

@timed
def fib_recursed(n):
    return fib_recursive(n)

使用例:

fib_recursed(10)
# 出力:
# fib_recursed(10) took 0.000421s to run.
# 戻り値: 55

ループアプローチ

ループを使った方法は、重複計算を避けるためより効率的です。

@timed
def fib_loop(n):
    fib_1, fib_2 = 1, 1
    for _ in range(3, n + 1):
        fib_1, fib_2 = fib_2, fib_1 + fib_2
    return fib_2

使用例:

fib_loop(10)
# 出力:
# fib_loop(10) took 0.000002s to run.
# 戻り値: 55

reduceを使った関数型プログラミング

`reduce`を使ってFibonacci数の計算を関数型スタイルで実装します。

from functools import reduce

@timed
def fib_reduce(n):
    initial = (1, 0)
    dummy = range(n)
    fib_n = reduce(lambda prev, _: (prev[0] + prev[1], prev[0]), dummy, initial)
    return fib_n[0]

使用例:

fib_reduce(10)
# 出力:
# fib_reduce(10) took 0.000003s to run.
# 戻り値: 55

パフォーマンスの分析

出力を比較すると、次のことが分かります。

  • 再帰的な方法は特に大きな`n`に対して非常に遅いです。

  • ループとreduceを使った方法ははるかに高速で、特にループの方法がやや速いです。

n = 35の場合のパフォーマンス比較:

fib_recursed(35)
# fib_recursed(35) took 3.556738s to run.

fib_loop(35)
# fib_loop(35) took 0.000004s to run.

fib_reduce(35)
# fib_reduce(35) took 0.000006s to run.

再帰的な方法の時間は指数関数的に増加しますが、ループとreduceの方法は効率的に処理されます。


ロガー・デコレーターの実装

関数呼び出しをログに記録するのも、デコレーターの実用的な応用です。これはデバッグや関数の使用状況の追跡に役立ちます。

ログデコレーターの作成

関数名と呼

び出された時間をログに記録する`logged`デコレーターを作成しましょう。

def logged(fn):
    from functools import wraps
    from datetime import datetime, timezone

    @wraps(fn)
    def inner(*args, **kwargs):
        run_dt = datetime.now(timezone.utc)
        result = fn(*args, **kwargs)
        print(f"{fn.__name__} was called at {run_dt}")
        return result

    return inner

説明:

  • モジュールのインポート:

    • `wraps`は関数のメタデータを保持します。

    • `datetime`と`timezone`はタイムスタンプを取得するために使います。

  • 内部関数の定義:

    • 現在のUTC時間を記録します。

    • オリジナルの関数`fn`を呼び出します。

    • 関数名とタイムスタンプを表示します。

  • 結果の返却:

    • 元の関数の結果を返します。

デコレーターの積み重ね

デコレーターは積み重ねてその効果を組み合わせることができます。デコレーターの適用順序は重要です。

例:

@timed
@logged
def factorial(n):
    from math import prod
    return prod(range(1, n + 1))

factorial(5)

出力:

factorial was called at 2024-08-25 06:36:06.994487+00:00
factorial(5) took 0.000002s to run.
# 戻り値: 120

積み重ねられたデコレーターの実行順序

デコレーターを積み重ねる際、関数定義に最も近いデコレーターが最初に適用されます。

  • `@timed @logged`の場合、`logged`が最初に適用され、次に`timed`が適用されます。

  • `timed`デコレーターは`logged(factorial)`の結果をラップします。

出力への影響:

  • `logged`デコレーターは`timed`よりも先に実行されます。

  • しかし、両方のデコレーターが元の関数を呼び出した後に出力を表示するため、タイミングの出力がログの出力よりも先に表示されます。


積み重ねられたデコレーターの実用的な応用

デコレーターを積み重ねる方法を理解すると、よりモジュール化され、保守性の高いコードを書くことができます。

関数のログとタイミング

`timed`と`logged`を積み重ねることで、関数コードを変更することなく、関数呼び出しを記録し、その実行時間を測定できます。

Web APIにおける認証とログ

Webアプリケーション、特にAPIでは、認証チェックとリクエストのログ記録が一般的です。

例:

@authenticated
@logged
def update_user_profile(user_id, data):
    # ユーザープロファイルの更新処理
    pass
  • 順序が重要:

    • `@authenticated`は、ユーザーが認証されていることを確認してから処理を進めます。

    • `@logged`は関数呼び出しを記録します。

  • 動作:

    • ユーザーが認証されていない場合、`update_user_profile`は呼び出されません。

    • ログの適用場所によって、認証に失敗した試行もログに記録できます。

デコレーター順序の調整:

  • 認証失敗も含めてすべての試行をログに記録したい場合、`@logged`を`@authenticated`の上に配置します。

  • 成功した呼び出しのみをログに記録したい場合は、`@authenticated`を最も外側のデコレーターにします。


結論

デコレーターは、関数をクリーンで保守しやすい形に強化できるPythonの強力な機能です。`timed`や`logged`のようなデコレーターを作成することで、関数のコアロジックを変更することなく、タイミング測定やログ記録といった機能を追加できます。

重要なポイント:

  • 関数のタイミング測定:

    • `timed`デコレーターは関数の実行時間を測定・報告します。

    • パフォーマンス分析や最適化に役立ちます。

  • 関数呼び出しのログ記録:

    • `logged`デコレーターは関数が呼び出された際にログを記録します。

    • デバッグや関数の使用状況の追跡に有用です。

  • デコレーターの積み重ね:

    • 複数のデコレーターを関数に適用できます。

    • デコレーターの適用順序は、動作と出力に影響を与えます。

  • 実用的な応用:

    • デコレーターはWebアプリケーションでの認証やキャッシングなどに使用できます。

    • コードの再利用性や関心の分離を促進します。

デコレーターを使いこなすことで、Pythonコードをより効率的で読みやすく、保守しやすく書くことができ、言語の強力な機能を最大限に活用できます。


次回の投稿では、パラメータ付きデコレーターを探求し、さらに柔軟なコードを作成する方法を紹介します!

「超本当にドラゴン」へ

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