PythonコードをDecoratorでクリーンに:コピペの悪夢からプロフェッショナルなプログラミングへ

Python tutorial - IT technology blog
Python tutorial - IT technology blog

「コピペ」による補助ロジックの悪夢

2022年、私は約45個の自動化スクリプトを含むモニタリングシステムを管理していました。これらのスクリプトは、DatabaseからRedis、Load Balancerまで、あらゆるもののヘルスチェック(health check)を行っていました。当時、パフォーマンス測定の要件があり、すべての関数で開始時間、終了時間、および総実行時間をログに記録する必要がありました。

駆け出しの頃、私は最も原始的な方法を選びました。各関数の中に飛び込み、最初と最後にtime.time()を挿入したのです。その結果、45箇所に全く同じコードが出現することになりました:

import time

def check_database_health():
    start = time.time()
    # 複雑なDBチェックロジック
    print("Checking Database...")
    time.sleep(1)
    
    end = time.time()
    print(f"Execution time: {end - start}s")

def check_redis_health():
    start = time.time()
    # Redisチェックロジック
    print("Checking Redis...")
    time.sleep(0.5)
    
    end = time.time()
    print(f"Execution time: {end - start}s")

要件が変更されたとき、事態は悪化しました。コンソールへの出力ではなく、JSON形式でファイルにログを記録することになったのです。45個の関数をすべて修正して回るのに午後の大半を費やし、ミスも多発しました。この時、自分がDRY(Don’t Repeat Yourself)の原則にひどく違反していることに気づきました。

ロジック混在の罠

問題は手間がかかることだけではありません。ビジネスロジック(business logic)と、ロギングや認証(auth)のような補助的なロジックを混ぜてしまうと、コードが非常に読みにくくなります。同僚がコードを見たとき、関数の本来の目的を知るために、時間計測のコードという「雑音」をかき分けなければなりません。

技術的には、これはCross-cutting Concerns(横断的関心事)と呼ばれます。これらはアプリケーションの多くの層にまたがって現れる機能です。もし、多くの関数の最初と最後で同じコード構造を繰り返しているなら、それはDecoratorを導入すべきサインです。

解決策:手動のWrapperからプロフェッショナルなDecoratorへ

1. Wrapper関数の使用

直接修正する代わりに、時間計測のロジックを外側の関数(wrapper)に切り出しました。これにより、コードの責務をある程度分離できます:

def timer_wrapper(func):
    def wrapper():
        start = time.time()
        func()
        end = time.time()
        print(f"Time taken: {end - start}s")
    return wrapper

def my_task():
    print("Doing something...") # 何かを実行中...

# 使用例
wrapped_task = timer_wrapper(my_task)
wrapped_task()

ロジックは分離できましたが、wrapped_task()を呼び出す方法はまだ冗長です。もし何百もの関数がある場合、このように手動で変数を割り当てるのは管理上の災難となります。

2. @Decorator構文を使いこなす

Pythonはこの問題をエレガントに解決するために@構文を提供しています。Decoratorの本質は、ある関数を引数として受け取り、その関数の「アップグレード版」を返す関数です。これによりコードがクリーンになり、元の関数名を維持したまま、メンテナンスが非常に容易になります。

シニアレベルのDecorator実装

Decoratorをあらゆる種類の関数(引数あり・なし)に対して柔軟に動作させるには、*args**kwargsを使用する必要があります。以下は、私が実際のプロジェクトで常に使用している構造です:

import time
from functools import wraps

def logger_decorator(func):
    @wraps(func) # 元のメタデータ(関数名、docstring)を保持する
    def wrapper(*args, **kwargs):
        print(f"--- 実行開始: {func.__name__} ---")
        start_time = time.time()
        
        result = func(*args, **kwargs) 
        
        end_time = time.time()
        print(f"--- 完了: {func.__name__} 実行時間: {end_time - start_time:.4f}秒 ---")
        return result
    return wrapper

@logger_decorator
def sync_data(source, destination):
    """サーバー間のデータ同期をシミュレートする"""
    print(f"{source} から {destination} へデータを転送中...")
    time.sleep(2)
    return "Success"

重要な注意点: 常に@wraps(func)を使用してください。これがないと、sync_data.__name__プロパティが'wrapper'に書き換わってしまいます。これはデバッグや自動ドキュメント生成を非常に困難にします。

DevOpsとバックエンドにおける3つの実用例

Decoratorは実行時間をログに記録するためだけのものではありません。実際には、以下の3つのシナリオでよく適用されます:

  • Retry Logic: ネットワークエラーが発生した場合、失敗を報告する前にAPIを3〜5回自動的に再試行します。
  • 権限管理 (RBAC): データベースの削除などの機密性の高いコマンドを実行する前に、Admin権限を確認します。
  • Caching: 重いクエリの結果をRedisに保存し、繰り返されるタスクでのDB負荷を最大70%削減します。

アクセス権限を制御するDecoratorের例:

def require_admin(func):
    @wraps(func)
    def wrapper(user_role, *args, **kwargs):
        if user_role != "admin":
            print("拒否:このアクションを実行する権限がありません!")
            return None
        return func(user_role, *args, **kwargs)
    return wrapper

@require_admin
def delete_system_logs(user_role):
    print("システムログを削除中...")

結び

Decoratorは決して難解な技術ではありません。それはコードの構成に関する異なる考え方です。コードを垂直方向(関数内にコマンドを追加する)に書く代わりに、レイヤー(層)で包み込むのです。

もし補助的なロジックを3つ以上の異なる関数で繰り返し書いていることに気づいたら、5分間手を止めてみてください。それをDecoratorに変換することで、将来のメンテナンス時間を大幅に節約できます。あなたのコードはプロフェッショナルで読みやすく、そして何より拡張性が高くなるでしょう。

Share: