一般社団法人 全国個人事業主支援協会

COLUMN コラム

  • Pythonのデコレータを活用したクリーンなコード設計パターン

デコレータが変えるコードの品質

Pythonのデコレータは、関数やクラスに横断的な関心事を付加するための仕組みです。ログ出力、認証チェック、キャッシュ、バリデーションなど、ビジネスロジックとは直接関係のない処理を分離することで、コードの可読性と保守性を大幅に向上させます。

現場では「デコレータは知っているが、自作したことがない」というエンジニアを多く見かけます。しかし、デコレータのパターンを理解し適切に設計できるようになると、コードベース全体の品質が一段階引き上がります。本記事では、実務で即活用できるデコレータの設計パターンを紹介します。

デコレータの基本構造

シンプルなデコレータ

デコレータの本質は「関数を受け取り、関数を返す高階関数」です。まずは最も基本的な形から確認しましょう。

import functools
import time

def timer(func):
"""関数の実行時間を計測するデコレータ"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} の実行時間: {elapsed:.4f}秒")
return result
return wrapper

@timer
def heavy_computation(n):
return sum(i * i for i in range(n))

heavy_computation(1000000)

functools.wrapsは必ず付けるべきです。これを忘れると、デコレートされた関数の__name____doc__が失われ、デバッグ時に困ることになります。

引数付きデコレータ

デコレータに設定値を渡したい場合は、もう一層ネストが必要になります。この三重構造は最初は混乱しやすいですが、パターンとして覚えてしまえば問題ありません。

def retry(max_attempts=3, delay=1.0, exceptions=(Exception,)):
"""失敗時にリトライするデコレータ"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt < max_attempts:
print(f"{func.__name__} 失敗 (試行{attempt}/{max_attempts}), {delay}秒後にリトライ")
time.sleep(delay)
raise last_exception
return wrapper
return decorator

@retry(max_attempts=5, delay=2.0, exceptions=(ConnectionError, TimeoutError))
def fetch_api_data(url):
# API呼び出し処理
pass

実務で役立つデコレータパターン

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

関数の引数を事前検証するデコレータは、防御的プログラミングの観点から非常に有用です。特にチーム開発において、関数の使い方を制約として明示できます。

def validate_types(**type_hints):
"""引数の型を検証するデコレータ"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
import inspect
sig = inspect.signature(func)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
for param_name, expected_type in type_hints.items():
if param_name in bound.arguments:
value = bound.arguments[param_name]
if not isinstance(value, expected_type):
raise TypeError(
f"{param_name}は{expected_type.__name__}型である必要があります。"
f"実際の型: {type(value).__name__}"
)
return func(*args, **kwargs)
return wrapper
return decorator

@validate_types(user_id=int, email=str)
def create_user(user_id, email, name="unknown"):
return {"id": user_id, "email": email, "name": name}

キャッシュデコレータ

標準ライブラリのfunctools.lru_cacheは優秀ですが、TTL(有効期限)付きキャッシュが必要な場面も多いです。以下はシンプルなTTL付きキャッシュの実装例です。

def ttl_cache(ttl_seconds=300):
"""TTL付きキャッシュデコレータ"""
def decorator(func):
cache = {}

@functools.wraps(func)
def wrapper(*args):
now = time.time()
if args in cache:
result, cached_at = cache[args]
if now - cached_at < ttl_seconds:
return result
result = func(*args)
cache[args] = (result, now)
return result

wrapper.cache_clear = lambda: cache.clear()
return wrapper
return decorator

@ttl_cache(ttl_seconds=60)
def get_user_profile(user_id):
# DBからユーザー情報を取得
pass

ログ出力デコレータ

構造化ログとの組み合わせは、本番運用において非常に強力です。関数の入出力を自動記録することで、障害調査の効率が劇的に向上します。

import logging
import json

def log_io(logger=None, level=logging.INFO):
"""関数の入出力をログ出力するデコレータ"""
def decorator(func):
nonlocal logger
if logger is None:
logger = logging.getLogger(func.__module__)

@functools.wraps(func)
def wrapper(*args, **kwargs):
func_name = f"{func.__module__}.{func.__name__}"
logger.log(level, json.dumps({
"event": "function_call",
"function": func_name,
"args_count": len(args),
"kwargs_keys": list(kwargs.keys()),
}))
try:
result = func(*args, **kwargs)
logger.log(level, json.dumps({
"event": "function_return",
"function": func_name,
"success": True,
}))
return result
except Exception as e:
logger.error(json.dumps({
"event": "function_error",
"function": func_name,
"error_type": type(e).__name__,
"error_message": str(e),
}))
raise
return wrapper
return decorator

デコレータ設計のベストプラクティス

実務でデコレータを設計する際に意識すべきポイントをまとめます。

  • 単一責任の原則を守る:1つのデコレータに複数の責務を持たせない。組み合わせで対応する
  • functools.wrapsを必ず使う:元の関数のメタデータを保持する
  • 副作用を最小限にする:デコレータが予期しない状態変更を起こさないようにする
  • テスト容易性を確保する:デコレータを外した状態でもテストできる設計にする
  • デコレータの順序に注意する:複数のデコレータを重ねる場合、適用順序が下から上であることを意識する

まとめ

デコレータは、横断的関心事をビジネスロジックから分離するための強力なツールです。基本パターンを理解した上で、リトライ、バリデーション、キャッシュ、ログなど実務で頻出するユースケースに適用していくことで、コードベース全体のクリーンさが向上します。まずはプロジェクト内で共通化できる処理を見つけ、デコレータとして切り出すところから始めてみてください。

この記事をシェアする

  • Twitterでシェア
  • Facebookでシェア
  • LINEでシェア