Pythonのデコレータを実務で使いこなす

はじめに

Pythonのデコレータは「関数やクラスに手を加えずに、その前後へ処理を差し込む」ための仕組みです。入門書では @ 記号の使い方までは説明されても、実務でどこに効くのかまでは踏み込まないことが多いと感じます。

この記事では、デコレータの基礎から、ログ・計測・リトライ・キャッシュ・認可チェックといった実務で頻出するパターンまでを、そのままコピーして使えるコード付きで整理します。対象は「文法は知っているが、自分で書く場面がまだ掴めていない」方です。動作確認は Python 3.11、追加ライブラリなし(標準ライブラリの functools のみ)です。

デコレータとは何か

デコレータの正体は「関数を受け取り、関数を返す関数」です。まずは糖衣構文を使わずに書いてみます。

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("前処理")
        result = func(*args, **kwargs)
        print("後処理")
        return result
    return wrapper


def hello():
    print("hello")


hello = my_decorator(hello)  # ← ここがポイント
hello()

hello = my_decorator(hello) を糖衣構文で書いたものが @my_decorator です。

@my_decorator
def hello():
    print("hello")

この2つはまったく同じ意味です。デコレータは「魔法」ではなく、単なる関数の再代入であると理解すると、以降の応用が一気に読めるようになります。

最初に覚えるべき functools.wraps

素朴に wrapper を返すと、元の関数の名前やdocstringが wrapper のものに置き換わってしまいます。これはデバッグやドキュメント生成のときに困ります。実務でデコレータを書くときは、functools.wraps はほぼ必須の作法です。

from functools import wraps


def good(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper


@good
def greet():
    """あいさつする"""
    ...


print(greet.__name__)  # greet
print(greet.__doc__)   # あいさつする

パターン1: 実行時間の計測

「どの処理が遅いのか」を調べる最初の一歩として、時間計測デコレータが便利です。

import time
from functools import wraps


def timeit(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            elapsed = time.perf_counter() - start
            print(f"{func.__name__} took {elapsed:.3f}s")
    return wrapper


@timeit
def heavy():
    time.sleep(0.5)


heavy()  # heavy took 0.500s

finally に入れているのは、例外が発生しても計測結果を残すためです。

パターン2: 引数付きデコレータ(3階建て構造)

「リトライ回数を指定したい」など、デコレータ自体にパラメータを渡したい場面があります。このとき関数が3階建てになります。

import time
from functools import wraps


def retry(times=3, delay=1.0, exceptions=(Exception,)):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_error = None
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_error = e
                    print(f"{func.__name__} failed ({attempt}/{times}): {e}")
                    if attempt < times:
                        time.sleep(delay)
            raise last_error
        return wrapper
    return decorator


@retry(times=3, delay=0.5, exceptions=(ConnectionError,))
def fetch():
    ...
  • 一番外の retry … パラメータを受け取る
  • 真ん中の decorator … 関数を受け取る
  • 一番内の wrapper … 実際の呼び出しを包む

「引数付きデコレータは1階層深くなる」とだけ覚えておけば迷いません。

パターン3: 結果のキャッシュ

同じ引数で何度も呼ばれる純粋関数は、標準ライブラリの functools.lru_cache でキャッシュできます。自作せずまずこれを検討します。

from functools import lru_cache


@lru_cache(maxsize=128)
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)


print(fib(50))
print(fib.cache_info())  # ヒット率などが見られる

注意点として、キャッシュキーは引数から作られるため、引数はハッシュ可能(リストや辞書は不可)である必要があります。副作用のある関数や毎回結果が変わる関数には使わないでください。

パターン4: 認可チェック(Web開発での定番)

Webフレームワークでは「ログイン必須」「管理者のみ」といったチェックをデコレータで表現するのが定番です。ここではフレームワーク非依存の擬似コードで骨格を示します。

from functools import wraps


def require_role(role):
    def decorator(func):
        @wraps(func)
        def wrapper(user, *args, **kwargs):
            if role not in user.get("roles", []):
                raise PermissionError(f"role '{role}' required")
            return func(user, *args, **kwargs)
        return wrapper
    return decorator


@require_role("admin")
def delete_account(user, target_id):
    print(f"{target_id} を削除しました")

処理の本体と「入る前のチェック」を分離できるのがデコレータの価値です。本体側は業務ロジックだけに集中できます。

パターン5: 複数デコレータの重ねがけと順序

デコレータは重ねられますが、適用順序で挙動が変わります。

@timeit
@retry(times=2)
def task():
    ...

これは timeit(retry(times=2)(task)) と等価です。下(内側)から適用され、実行は上(外側)から入ります。つまり timeit がリトライ全体を含めた時間を計測します。1回あたりを計測したければ順序を入れ替えます。「近い方が先に関数を包む」と覚えておくと、順序の混乱を防げます。

注意点

  • 乱用しない: 制御フローが見えづらくなるため、「横断的関心事(ログ・計測・認可・キャッシュ)」に限定するのが実務での目安です。
  • functools.wraps を必ず付ける: 付け忘れはデバッグ時の地雷になります。
  • 引数の素通し: *args, **kwargs を通さないと、汎用デコレータが特定シグネチャの関数でしか使えなくなります。
  • 例外の握りつぶしに注意: wrapper 内で例外を握って握りつぶすと、呼び出し側が失敗に気づけません。ログを残すか再送出しましょう。

まとめ

デコレータは「関数を受け取り関数を返す」だけの素朴な仕組みです。ここに functools.wraps を添え、計測・リトライ・キャッシュ・認可といった横断的関心事へ適用すると、業務ロジックを汚さずに共通処理を差し込めます。まずは timeit のような単純なものから自作し、慣れたら引数付き(3階建て)へ進むのがおすすめです。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。