Pythonデコレータ実務パターン:業務効率化の実装テクニック

Python

Pythonデコレータ実務パターン:業務効率化の実装テクニック

デコレータとは何か

Pythonのデコレータは、関数やクラスの振る舞いを変更または拡張する仕組みです。本来の機能を変えずに、前処理や後処理を追加したり、複数の関数に共通の処理を適用したりできます。

デコレータの基本形は以下の通りです:

def decorator(func):\n    def wrapper(*args, **kwargs):\n        # 前処理\n        result = func(*args, **kwargs)\n        # 後処理\n        return result\n    return wrapper\n\n@decorator\ndef my_function():\n    pass

構文では@decoratorという形で関数の直前に記述し、その関数の振る舞いをカスタマイズします。

業務でのユースケース

実務開発では、デコレータは以下のような場面で活躍します:

  • ログ記録:関数の実行時間や入出力値をログに記録
  • 認証・認可:APIエンドポイントへのアクセス権限をチェック
  • キャッシング:計算結果をメモリに保持して効率化
  • エラーハンドリング:例外を一括処理し、リトライロジックを実装
  • バリデーション:関数の入力値を自動検証
  • レート制限:APIの呼び出し頻度を制御

これらの処理を毎回手動で書くのは非効率なため、デコレータで共通化すれば、装飾するだけで機能が適用できます。

実装コード:実務で使うパターン

1. 実行時間計測デコレータ

関数の処理時間を計測し、性能調査や遅いの特定に役立ちます。データ処理やAPI呼び出し前後で実際に何度も使うパターンです。

import time\nimport functools\nfrom typing import Any, Callable\n\ndef measure_execution_time(func: Callable) -> Callable:\n    \"\"\"関数の実行時間を計測してログに出力するデコレータ\"\"\"\n    @functools.wraps(func)\n    def wrapper(*args, **kwargs) -> Any:\n        start_time = time.time()\n        try:\n            result = func(*args, **kwargs)\n            return result\n        finally:\n            elapsed_time = time.time() - start_time\n            print(f\"[{func.__name__}] 実行時間: {elapsed_time:.3f}秒\")\n    return wrapper\n\n@measure_execution_time\ndef fetch_user_data(user_id: int) -> dict:\n    \"\"\"ユーザーデータを取得する重い処理\"\"\"\n    time.sleep(1.5)  # 実際のAPI呼び出しを想定\n    return {\"user_id\": user_id, \"name\": \"太郎\"}\n\n# 使用例\nresult = fetch_user_data(123)\nprint(result)\n# 出力: [fetch_user_data] 実行時間: 1.502秒\n# {'user_id': 123, 'name': '太郎'}

2. リトライロジック付きデコレータ

ネットワーク処理やAPI呼び出しで失敗することがあります。自動でリトライする仕組みを実装すれば、一時的なエラーに強くなります。

import functools\nimport random\nfrom typing import Any, Callable, Type\n\ndef retry_on_exception(\n    max_attempts: int = 3,\n    delay: float = 1.0,\n    exceptions: tuple = (Exception,)\n) -> Callable:\n    \"\"\"指定した例外が発生したら自動でリトライするデコレータ\"\"\"\n    def decorator(func: Callable) -> Callable:\n        @functools.wraps(func)\n        def wrapper(*args, **kwargs) -> Any:\n            attempt = 0\n            last_exception = None\n            \n            while attempt < max_attempts:\n                try:\n                    return func(*args, **kwargs)\n                except exceptions as e:\n                    attempt += 1\n                    last_exception = e\n                    if attempt < max_attempts:\n                        print(f\"[{func.__name__}] 失敗(試行 {attempt}/{max_attempts})。\" \n                              f\"{delay}秒後に再試行...\")\n                        time.sleep(delay)\n                    else:\n                        print(f\"[{func.__name__}] {max_attempts}回の試行後も失敗\")\n            \n            raise last_exception\n        return wrapper\n    return decorator\n\n@retry_on_exception(max_attempts=3, delay=0.5, exceptions=(ConnectionError, TimeoutError))\ndef call_external_api(endpoint: str) -> dict:\n    \"\"\"外部APIを呼び出す\"\"\"\n    # 実装例:10%の確率で失敗\n    if random.random() < 0.1:\n        raise ConnectionError(f\"API接続失敗: {endpoint}\")\n    return {\"status\": \"success\", \"data\": []}\n\n# 使用例\ntry:\n    result = call_external_api(\"/api/users\")\n    print(result)\nexcept Exception as e:\n    print(f\"エラー: {e}\")

3. ログ記録デコレータ

関数の入出力をログに記録することで、デバッグやモニタリングが容易になります。本番環境でもよく使う重要なパターンです。

import functools\nimport logging\nfrom typing import Any, Callable\nimport json\n\nlogger = logging.getLogger(__name__)\n\ndef log_function_call(log_args: bool = True, log_result: bool = True) -> Callable:\n    \"\"\"関数の呼び出し、引数、戻り値をログに記録するデコレータ\"\"\"\n    def decorator(func: Callable) -> Callable:\n        @functools.wraps(func)\n        def wrapper(*args, **kwargs) -> Any:\n            func_name = func.__name__\n            \n            # 入力ログ\n            if log_args:\n                logger.info(\n                    f\"関数開始: {func_name}\",\n                    extra={\n                        \"function\": func_name,\n                        \"args\": str(args)[:200],  # 長すぎる場合はカット\n                        \"kwargs\": json.dumps(kwargs, default=str, ensure_ascii=False)[:200]\n                    }\n                )\n            \n            try:\n                result = func(*args, **kwargs)\n                \n                # 出力ログ\n                if log_result:\n                    logger.info(\n                        f\"関数終了: {func_name}\",\n                        extra={\n                            \"function\": func_name,\n                            \"result\": str(result)[:200]\n                        }\n                    )\n                \n                return result\n            except Exception as e:\n                logger.exception(\n                    f\"関数エラー: {func_name}\",\n                    extra={\n                        \"function\": func_name,\n                        \"exception\": str(e)\n                    }\n                )\n                raise\n        \n        return wrapper\n    return decorator\n\n@log_function_call(log_args=True, log_result=True)\ndef process_order(order_id: int, amount: float) -> dict:\n    \"\"\"注文を処理する\"\"\"\n    if amount <= 0:\n        raise ValueError(\"金額は正数である必要があります\")\n    return {\"order_id\": order_id, \"status\": \"completed\", \"amount\": amount}\n\n# 使用例\nresult = process_order(12345, 9999.99)

4. キャッシング機能付きデコレータ

同じ引数で関数が呼ばれた場合、前回の計算結果を再利用します。頻繁に同じクエリを実行するDB検索やAPI呼び出しで大きな効果があります。

import functools\nfrom typing import Any, Callable, Dict, Hashable\nimport time\n\ndef memoize_with_ttl(ttl_seconds: int = 60) -> Callable:\n    \"\"\"計算結果をキャッシュし、TTL付きで無効化するデコレータ\"\"\"\n    def decorator(func: Callable) -> Callable:\n        cache: Dict[Hashable, tuple] = {}  # (result, timestamp)\n        \n        @functools.wraps(func)\n        def wrapper(*args, **kwargs) -> Any:\n            # キャッシュキーを生成\n            cache_key = (args, tuple(sorted(kwargs.items())))\n            \n            # キャッシュを確認\n            if cache_key in cache:\n                result, cached_time = cache[cache_key]\n                elapsed = time.time() - cached_time\n                \n                if elapsed < ttl_seconds:\n                    print(f\"[{func.__name__}] キャッシュから取得({elapsed:.1f}秒経過)\")\n                    return result\n                else:\n                    # TTL切れ\n                    del cache[cache_key]\n            \n            # 新規実行\n            result = func(*args, **kwargs)\n            cache[cache_key] = (result, time.time())\n            print(f\"[{func.__name__}] 新規実行してキャッシュに保存\")\n            return result\n        \n        # キャッシュをクリアするメソッドを追加\n        def clear_cache():\n            cache.clear()\n        \n        wrapper.clear_cache = clear_cache\n        return wrapper\n    \n    return decorator\n\n@memoize_with_ttl(ttl_seconds=5)\ndef get_user_info(user_id: int) -> dict:\n    \"\"\"ユーザー情報を取得(キャッシュあり)\"\"\"\n    time.sleep(1)  # 重い処理を想定\n    return {\"user_id\": user_id, \"name\": f\"User{user_id}\"}\n\n# 使用例\nprint(get_user_info(1))  # 新規実行\nprint(get_user_info(1))  # キャッシュから取得\ntime.sleep(6)\nprint(get_user_info(1))  # TTL切れなので新規実行\nget_user_info.clear_cache()  # 手動でキャッシュクリア

5. バリデーションデコレータ

関数の引数を自動検証し、不正な入力を事前に防ぎます。ビジネスロジックの前に入力チェックを一元化できます。

import functools\nfrom typing import Any, Callable, Dict, Optional\n\ndef validate_input(\n    schema: Dict[str, type],\n    required_keys: Optional[list] = None\n) -> Callable:\n    \"\"\"辞書形式の入力を検証するデコレータ\"\"\"\n    if required_keys is None:\n        required_keys = list(schema.keys())\n    \n    def decorator(func: Callable) -> Callable:\n        @functools.wraps(func)\n        def wrapper(*args, **kwargs) -> Any:\n            # 最初の引数が辞書として検証対象と仮定\n            if args and isinstance(args[0], dict):\n                data = args[0]\n                \n                # 必須キーのチェック\n                missing_keys = set(required_keys) - set(data.keys())\n                if missing_keys:\n                    raise ValueError(f\"必須キーが不足しています: {missing_keys}\")\n                \n                # 型チェック\n                for key, expected_type in schema.items():\n                    if key in data and not isinstance(data[key], expected_type):\n                        actual_type = type(data[key]).__name__\n                        expected_type_name = expected_type.__name__\n                        raise TypeError(\n                            f\"キー '{key}' の型が不正です。\"\n                            f\"期待: {expected_type_name}, 実際: {actual_type}\"\n                        )\n            \n            return func(*args, **kwargs)\n        return wrapper\n    return decorator\n\n@validate_input(\n    schema={\n        \"user_id\": int,\n        \"email\": str,\n        \"age\": int,\n        \"is_active\": bool\n    },\n    required_keys=[\"user_id\", \"email\"]\n)\ndef create_user(data: dict) -> dict:\n    \"\"\"ユーザーを作成する\"\"\"\n    return {\"status\": \"created\", \"data\": data}\n\n# 使用例\ntry:\n    # 成功例\n    result = create_user({\n        \"user_id\": 1,\n        \"email\": \"user@example.com\",\n        \"age\": 30,\n        \"is_active\": True\n    })\n    print(\"成功:\", result)\n    \n    # 失敗例:必須キー不足\n    create_user({\"user_id\": 2})  # EmailがないのでValueError\nexcept (ValueError, TypeError) as e:\n    print(f\"検証エラー: {e}\")

よくある応用パターン

デコレータチェーン

複数のデコレータを組み合わせることで、処理を積み重ねられます。実務では複数の関心事を同時に扱う場面が多いため、重要なパターンです。

import functools\nimport time\n\ndef timer(func):\n    @functools.wraps(func)\n    def wrapper(*args, **kwargs):\n        start = time.time()\n        result = func(*args, **kwargs)\n        print(f\"実行時間: {time.time() - start:.3f}秒\")\n        return result\n    return wrapper\n\ndef logger_dec(func):\n    @functools.wraps(func)\n    def wrapper(*args, **kwargs):\n        print(f\"実行開始: {func.__name__}\")\n        result = func(*args, **kwargs)\n        print(f\"実行終了: {func.__name__}\")\n        return result\n    return wrapper\n\n# 複数のデコレータを適用(下から順に適用される)\n@timer\n@logger_dec\ndef complex_calculation(n: int) -> int:\n    \"\"\"複雑な計算を行う\"\"\"\n    time.sleep(0.5)\n    return n * n\n\nresult = complex_calculation(10)\n# 出力:\n# 実行開始: complex_calculation\n# 実行終了: complex_calculation\n# 実行時間: 0.502秒

クラスメソッド用デコレータ

クラスメソッドに対してもデコレータを適用できます。データベースアクセスオブジェクト(DAO)やAPIクライアントの実装で頻繁に使われます。

import functools\nfrom typing import Any, Callable\n\ndef requires_database_connection(func: Callable) -> Callable:\n    \"\"\"データベース接続をチェックするデコレータ\"\"\"\n    @functools.wraps(func)\n    def wrapper(self, *args, **kwargs) -> Any:\n        if not hasattr(self, 'db_connection') or self.db_connection is None:\n            raise RuntimeError(f\"データベース接続が確立されていません\")\n        return func(self, *args, **kwargs)\n    return wrapper\n\nclass UserRepository:\n    def __init__(self, db_connection=None):\n        self.db_connection = db_connection\n    \n    @requires_database_connection\n    def get_user(self, user_id: int) -> dict:\n        \"\"\"ユーザーを取得\"\"\"\n        # 実際のDB処理\n        return {\"user_id\": user_id, \"name\": \"太郎\"}\n    \n    @requires_database_connection\n    def create_user(self, name: str, email: str) -> dict:\n        \"\"\"ユーザーを作成\"\"\"\n        return {\"id\": 123, \"name\": name, \"email\": email}\n\n# 使用例\nrepo = UserRepository(db_connection=None)\ntry:\n    repo.get_user(1)  # RuntimeError: データベース接続が確立されていません\nexcept RuntimeError as e:\n    print(f\"エラー: {e}\")\n\n# 接続を確立\nrepo.db_connection = \"connected\"\nresult = repo.get_user(1)  # 成功\nprint(result)

デコレータ実装時の注意点

1. functools.wrapsを使う

デコレータで元の関数の情報(名前やドキュメント)が失われるため、functools.wrapsで引き継ぐ必要があります。これはLogging、APIドキュメント生成、デバッグ時に重要です。

import functools\n\n# 悪い例\ndef bad_decorator(func):\n    def wrapper(*args, **kwargs):\n        return func(*args, **kwargs)\n    return wrapper\n\n@bad_decorator\ndef my_func():\n    \"\"\"これは重要なドキュメント\"\"\"\n    pass\n\nprint(my_func.__name__)  # 'wrapper' (元の名前が失われている)\nprint(my_func.__doc__)   # None (ドキュメントが失われている)\n\n# 良い例\ndef good_decorator(func):\n    @functools.wraps(func)  # これが重要\n    def wrapper(*args, **kwargs):\n        return func(*args, **kwargs)\n    return wrapper\n\n@good_decorator\ndef my_func2():\n    \"\"\"これは重要なドキュメント\"\"\"\n    pass\n\nprint(my_func2.__name__)  # 'my_func2' (元の名前を保持)\nprint(my_func2.__doc__)   # \"これは重要なドキュメント\" (ドキュメントを保持)

2. 例外処理とリソース管理

デコレータで前処理と後処理を行う場合、例外が発生しても後処理が実行されるようにする必要があります。try-finallyやコンテキストマネージャを活用します。

import functools\nfrom contextlib import contextmanager\n\ndef with_resource_management(func):\n    \"\"\"リソース管理を行うデコレータ\"\"\"\n    @functools.wraps(func)\n    def wrapper(*args, **kwargs):\n        # リソース取得\n        resource = acquire_resource()\n        print(f\"リソース取得: {resource}\")\n        \n        try:\n            # 関数実行\n            result = func(*args, **kwargs)\n            return result\n        except Exception as e:\n            print(f\"エラー発生: {e}\")\n            raise\n        finally:\n            # 必ず実行(例外が発生してもリソースを解放)\n            release_resource(resource)\n            print(f\"リソース解放: {resource}\")\n    \n    return wrapper\n\ndef acquire_resource():\n    return \"DB_CONNECTION\"\n\ndef release_resource(resource):\n    pass\n\n@with_resource_management\ndef database_operation():\n    \"\"\"DB操作を実行\"\"\"\n    print(\"DB処理中...\")\n    # エラーが発生してもリソースは解放される\n    return \"success\"\n\nresult = database_operation()\n# 出力:\n# リソース取得: DB_CONNECTION\n# DB処理中...\n# リソース解放: DB_CONNECTION

3. パフォーマンスへの配慮

デコレータは全関数実行時に動作するため、過度なオーバーヘッドがないか注意が必要です。特にループ内で頻繁に呼ばれる関数にデコレータを適用する場合は、性能測定が重要です。

import functools\nimport time\n\ndef performance_critical_decorator(func):\n    \"\"\"最小限のオーバーヘッドで機能するデコレータ\"\"\"\n    @functools.wraps(func)\n    def wrapper(*args, **kwargs):\n        # 軽量な処理のみ\n        # ログ出力、重い計算は避ける\n        return func(*args, **kwargs)\n    return wrapper\n\n# パフォーマンス測定\nimport timeit\n\ndef regular_function(x):\n    return x * 2\n\n@performance_critical_decorator\ndef decorated_function(x):\n    return x * 2\n\n# 100万回の実行時間を計測\nregular_time = timeit.timeit(\n    lambda: regular_function(5),\n    number=1000000\n)\ndecorated_time = timeit.timeit(\n    lambda: decorated_function(5),\n    number=1000000\n)\n\nprint(f\"通常: {regular_time:.3f}秒\")\nprint(f\"デコレータ: {decorated_time:.3f}秒\")\nprint(f\"オーバーヘッド: {(decorated_time - regular_time) * 1000:.3f}ms\")

4. スタック可能性の確保

複数のデコレータを組み合わせる場合、引数の形式に統一性を持たせることが重要です。パラメータなしのデコレータとパラメータありのデコレータを混在させるときは特に注意が必要です。

import functools\nfrom typing import Any, Callable, Optional\n\n# パラメータなし\ndef simple_decorator(func: Callable) -> Callable:\n    @functools.wraps(func)\n    def wrapper(*args, **kwargs) -> Any:\n        print(\"Simple decorator\")\n        return func(*args, **kwargs)\n    return wrapper\n\n# パラメータあり\ndef parameterized_decorator(param: str = \"default\") -> Callable:\n    def decorator(func: Callable) -> Callable:\n        @functools.wraps(func)\n        def wrapper(*args, **kwargs) -> Any:\n            print(f\"Parameterized decorator: {param}\")\n            return func(*args, **kwargs)\n        return wrapper\n    return decorator\n\n# 統一的に組み合わせられるように\n@simple_decorator\n@parameterized_decorator(param=\"custom\")\ndef my_function():\n    print(\"Function execution\")\n\nmy_function()\n# 出力:\n# Simple decorator\n# Parameterized decorator: custom\n# Function execution

実務での応用例:APIサーバーの実装

以下は、FastAPIなどのフレームワークで使用される実務的な例です。複数のデコレータを組み合わせて、実際のAPIサーバーに求められる機能を実装しています。

import functools\nimport time\nimport logging\nfrom typing import Any, Callable, Dict, Optional\nimport jwt\nfrom datetime import datetime\n\nlogger = logging.getLogger(__name__)\n\n# 1. 認証デコレータ\ndef require_auth(func: Callable) -> Callable:\n    \"\"\"APIの認証をチェック\"\"\"\n    @functools.wraps(func)\n    def wrapper(*args, request=None, **kwargs) -> Any:\n        if request is None or not hasattr(request, 'headers'):\n            raise PermissionError(\"リクエストが不正です\")\n        \n        token = request.headers.get('Authorization')\n        if not token:\n            raise PermissionError(\"トークンが見つかりません\")\n        \n        try:\n            # トークン検証(簡略版)\n            payload = jwt.decode(token.replace('Bearer ', ''), 'secret', algorithms=['HS256'])\n            request.user = payload\n        except jwt.InvalidTokenError:\n            raise PermissionError(\"無効なトークンです\")\n        \n        return func(*args, request=request, **kwargs)\n    \n    return wrapper\n\n# 2. レート制限デコレータ\ndef rate_limit(max_calls: int = 100, time_window: int = 60) -> Callable:\n    \"\"\"時間ウィンドウ内での呼び出し回数を制限\"\"\"\n    def decorator(func: Callable) -> Callable:\n        call_times = {}\n        \n        @functools.wraps(func)\n        def wrapper(*args, user_id: Optional[str] = None, **kwargs) -> Any:\n            if user_id is None:\n                user_id = \"anonymous\"\n            \n            now = time.time()\n            if user_id not in call_times:\n                call_times[user_id] = []\n            \n            # 時間ウィンドウ外の呼び出しを削除\n            call_times[user_id] = [\n                call_time for call_time in call_times[user_id]\n                if now - call_time < time_window\n            ]\n            \n            if len(call_times[user_id]) >= max_calls:\n                raise RuntimeError(\n                    f\"レート制限超過: {max_calls}回/{time_window}秒\"\n                )\n            \n            call_times[user_id].append(now)\n            return func(*args, user_id=user_id, **kwargs)\n        \n        return wrapper\n    return decorator\n\n# 3. ログ+タイミング+エラー処理\ndef api_handler(\n    operation_name: str,\n    log_request_body: bool = False\n) -> Callable:\n    \"\"\"API処理の標準的なハンドリング\"\"\"\n    def decorator(func: Callable) -> Callable:\n        @functools.wraps(func)\n        def wrapper(*args, request=None, **kwargs) -> Any:\n            request_id = getattr(request, 'id', 'unknown')\n            start_time = time.time()\n            \n            logger.info(\n                f\"[{request_id}] API開始: {operation_name}\",\n                extra={\n                    \"operation\": operation_name,\n                    \"timestamp\": datetime.now().isoformat()\n                }\n            )\n            \n            try:\n                result = func(*args, request=request, **kwargs)\n                \n                elapsed = time.time() - start_time\n                logger.info(\n                    f\"[{request_id}] API成功: {operation_name}\",\n                    extra={\n                        \"operation\": operation_name,\n                        \"elapsed_ms\": elapsed * 1000,\n                        \"status\": \"success\"\n                    }\n                )\n                \n                return result\n            \n            except Exception as e:\n                elapsed = time.time() - start_time\n                logger.error(\n                    f\"[{request_id}] API失敗: {operation_name}\",\n                    extra={\n                        \"operation\": operation_name,\n                        \"elapsed_ms\": elapsed * 1000,\n                        \"error\": str(e),\n                        \"error_type\

タイトルとURLをコピーしました