デコレータとは、関数(またはクラス)を受け取り、処理を足した新しい関数を返す関数のことです。 関数の上に書く @名前 は、func = 名前(func) を短く書くための記法(糖衣構文)です。元の関数の中身を変えずに、ログ・計測・リトライ・権限チェックなどを外から足せます。

この記事では、仕組みを段階的にコードと出力で確かめたうえで、functools.wraps の有無で何が変わるか、ParamSpec で型情報を保つ方法(mypy で確認)、そしてそのままコピーして使えるレシピ12選を紹介します。すべて Python 3.11.15 で実行し、3.12以降の新しい書き方と 3.13以降の warnings.deprecated は Python 3.14.7 で実行した結果を載せています。

デコレータの早見表

やりたいこと 書き方
基本形 def deco(func): → def wrapper(*args, **kwargs): → return wrapper
関数名・docstringを保つ wrapper に @functools.wraps(func)
型ヒントを保つ Callable[P, R](ParamSpec)
引数を渡す @retry(3) 関数を3重にする(引数→デコレータ→wrapper)
@timer と @timer(unit="s") 両対応 def timer(func=None, *, unit="ms")
状態(呼び出し回数)を持つ __call__ を持つクラス
重ねがけ 下(関数に近い方)から適用、呼び出しは上から
時間計測 time.perf_counter() + finally
リトライ 指数バックオフ+対象例外の指定
キャッシュ @functools.cache / @functools.lru_cache
ログ logging + log.exception()
引数検証 inspect.signature().bind()
権限チェック ContextVar で現在のユーザーを参照
非推奨警告 warnings.warn(..., DeprecationWarning)/3.13+は @warnings.deprecated
タイムアウト 同期はスレッド+result(timeout)、async は asyncio.timeout
シングルトン クラスデコレータ
登録(プラグイン) 辞書に入れて関数をそのまま返す
async 対応 inspect.iscoroutinefunction() で分岐
クラスを加工 クラスを受け取りクラスを返す

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Pythonデコレータの使い方|コピペで使えるレシピ12選」

デコレータの仕組みを3段階で理解する

ポイントは「Pythonの関数はオブジェクトなので、変数に入れたり引数に渡したり、戻り値にしたりできる」ことです。 これを使って、関数を包む関数を作ります。

# 1) 関数はオブジェクト:変数に入れたり、引数に渡したりできる
def hello():
    return "こんにちは"

f = hello
print(f())

# 2) 関数を受け取り、新しい関数を返す関数 = デコレータ
def shout(func):
    def wrapper():
        return func() + "!!"
    return wrapper

loud = shout(hello)       # 手で包む
print(loud())

# 3) @ は「bye = shout(bye)」の糖衣構文
@shout
def bye():
    return "さようなら"
print(bye())
print(bye.__name__)
こんにちは
こんにちは!!
さようなら!!
wrapper

最後の行に注目してください。 bye の中身は wrapper に置き換わったため、__name__ が wrapper になっています。これを直すのが次に説明する functools.wraps です。

どんな引数の関数にも使える形にする

wrapper を *args, **kwargs で受けて、そのまま元の関数に渡し、戻り値を返すのが汎用の型です。

def trace(func):
    def wrapper(*args, **kwargs):            # どんな引数でも受け取る
        print(f"呼び出し: {func.__name__}{args} {kwargs}")
        result = func(*args, **kwargs)       # 元の関数にそのまま渡す
        print(f"戻り値: {result!r}")
        return result                        # 戻り値を返し忘れない
    return wrapper

@trace
def add(a, b, *, tax=0):
    return (a + b) * (1 + tax)

print(add(100, 200, tax=0.1))
呼び出し: add(100, 200) {'tax': 0.1}
戻り値: 330.0
330.0

functools.wrapsの有無で何が変わるか

@functools.wraps(func) を wrapper に付けると、元の関数の名前・docstring・シグネチャ情報が引き継がれます。 付けた場合と付けない場合を並べて実行しました。

import functools, inspect

def no_wraps(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

def with_wraps(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@no_wraps
def total_a(price: int, qty: int = 1) -> int:
    """合計金額を返す"""
    return price * qty

@with_wraps
def total_b(price: int, qty: int = 1) -> int:
    """合計金額を返す"""
    return price * qty

for f in (total_a, total_b):
    print(f.__name__, "|", f.__doc__, "|", inspect.signature(f), "|",
          f.__wrapped__.__name__ if hasattr(f, "__wrapped__") else "-")
wrapper | None | (*args, **kwargs) | -
total_b | 合計金額を返す | (price: int, qty: int = 1) -> int | total_b

wraps なしでは名前・docstring・シグネチャがすべて wrapper のものになりました。 ログの関数名、help()、ドキュメント生成、FastAPI や pytest の引数解析に影響するので、付けるのが基本です。__wrapped__ からは元の関数も呼べます。

ParamSpecで型ヒントを保つ書き方

Callable[..., Any] で書くと、デコレートした関数の引数の型が型チェッカーから見えなくなります。 ParamSpec(3.10+)を使うと、元の関数の引数と戻り値の型をそのまま保てます。mypy 1.20.2 で確認しました。

import functools
from collections.abc import Callable
from typing import Any, ParamSpec, TypeVar

P = ParamSpec("P")
R = TypeVar("R")

def loose(func: Callable[..., Any]) -> Callable[..., Any]:
    @functools.wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        return func(*args, **kwargs)
    return wrapper

def typed(func: Callable[P, R]) -> Callable[P, R]:
    @functools.wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        return func(*args, **kwargs)
    return wrapper

@loose
def total_a(price: int, qty: int = 1) -> int:
    return price * qty

@typed
def total_b(price: int, qty: int = 1) -> int:
    return price * qty

reveal_type(total_a)
reveal_type(total_b)
total_a("100", qty="2")   # loose: 型の誤りに気づけない
total_b("100", qty="2")   # typed: 気づける
$ mypy s4_typed.py
s4_typed.py:28: note: Revealed type is "def (*Any, **Any) -> Any"
s4_typed.py:29: note: Revealed type is "def (price: int, qty: int =) -> int"
s4_typed.py:31: error: Argument 1 to "total_b" has incompatible type "str"; expected "int"  [arg-type]
s4_typed.py:31: error: Argument "qty" to "total_b" has incompatible type "str"; expected "int"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)

ParamSpec 版だけが、文字列を渡す誤りを検出できました。 Python 3.12 以降は def typed[**P, R](func: Callable[P, R]) -> Callable[P, R]: と書け、P = ParamSpec("P") の宣言を省けます(Python 3.14.7 + mypy で Revealed type is "def (price: int, qty: int =) -> int" を確認)。

型ヒント全般の書き方は別記事「Pythonの型ヒントの書き方」で扱っています。

コピペで使えるレシピ12選

以下はすべて 標準ライブラリだけで動き、そのまま貼って実行できるコードです(Python 3.11.15 で実行)。

レシピ1:実行時間を計測する

import functools, time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:                                   # 例外でも必ず計測結果を出す
            elapsed = time.perf_counter() - start
            print(f"[timer] {func.__name__}: {elapsed:.3f}s")
    return wrapper

@timer
def build_report(n):
    time.sleep(0.2)
    return sum(range(n))

print(build_report(1_000_000))
[timer] build_report: 0.242s
499999500000

計測には time.time() ではなく time.perf_counter() を使います。 時計合わせの影響を受けず、精度も高いためです。

レシピ2:リトライ(指数バックオフ)

import functools, random, time

def retry(times=3, delay=0.1, backoff=2.0, exceptions=(Exception,)):
    """失敗したら delay, delay*backoff, ... と間隔を広げて再実行する"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            wait = delay
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == times:
                        raise                       # 最後は元の例外をそのまま上げる
                    jitter = random.uniform(0, wait * 0.1)
                    print(f"[retry] {attempt}回目失敗: {e!r} → {wait + jitter:.2f}s 待機")
                    time.sleep(wait + jitter)
                    wait *= backoff
        return wrapper
    return decorator

calls = {"n": 0}

@retry(times=4, delay=0.1, exceptions=(ConnectionError,))
def fetch_stock():
    calls["n"] += 1
    if calls["n"] < 3:
        raise ConnectionError("一時的に接続できません")
    return {"item": "付箋", "stock": 8}

print(fetch_stock())
[retry] 1回目失敗: ConnectionError('一時的に接続できません') → 0.11s 待機
[retry] 2回目失敗: ConnectionError('一時的に接続できません') → 0.21s 待機
{'item': '付箋', 'stock': 8}

再試行する例外を exceptions で絞るのがポイントです。最後の試行で失敗したときは元の例外がそのまま上がります。ValueError のような「何度やっても同じ結果になる」エラーまで再試行すると、待ち時間だけが増えます。jitter(ゆらぎ)は、多数の処理が同時に再試行して集中するのを避けるためのものです。

レシピ3:キャッシュ(lru_cache / cache / 有効期限つき)

import functools, time

@functools.lru_cache(maxsize=128)          # 上限つき(古いものから捨てる)
def item_price(item_id: int) -> int:
    time.sleep(0.1)                        # 重い処理の代わり
    return item_id * 100

t = time.perf_counter(); item_price(1); item_price(1); item_price(2)
print(f"{time.perf_counter() - t:.2f}s", item_price.cache_info())

# 有効期限つきの自作キャッシュ
def ttl_cache(seconds):
    def decorator(func):
        store = {}
        @functools.wraps(func)
        def wrapper(*args):
            now = time.monotonic()
            if args in store and now - store[args][0] < seconds:
                return store[args][1]
            value = func(*args)
            store[args] = (now, value)
            return value
        return wrapper
    return decorator

@ttl_cache(seconds=0.2)
def now_stock(item_id):
    print(f"  (在庫を問い合わせ: {item_id})")
    return 10

now_stock(1); now_stock(1); time.sleep(0.25); now_stock(1)

try:
    item_price([1, 2])
except TypeError as e:
    print("TypeError:", e)
0.21s CacheInfo(hits=1, misses=2, maxsize=128, currsize=2)
  (在庫を問い合わせ: 1)
  (在庫を問い合わせ: 1)
TypeError: unhashable type: 'list'
方法 上限 有効期限 向いている場面
@functools.cache なし なし 入力の種類が限られる計算(再帰など)
@functools.lru_cache(maxsize=N) N件(古いものから破棄) なし 入力の種類が多い関数
自作 ttl_cache なし(必要なら追加) あり 時間で変わる値(在庫など)
@functools.cached_property インスタンスごと なし 重い計算をする属性

引数はハッシュ可能(タプル・文字列・数値など)である必要があります。 リストを渡すと上のとおり TypeError になるので、tuple(items) に変えて渡します。また、メソッドに lru_cache を付けると self もキャッシュのキーとして保持され続けるので、インスタンスが多いクラスでは cached_property か、インスタンス属性にキャッシュを持たせる方法が向いています。

レシピ4:呼び出しログを残す

import functools, logging

logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")

def log_call(logger=None, level=logging.INFO):
    def decorator(func):
        log = logger or logging.getLogger(func.__module__)
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            log.log(level, "開始 %s args=%r kwargs=%r", func.__qualname__, args, kwargs)
            try:
                result = func(*args, **kwargs)
            except Exception:
                log.exception("例外 %s", func.__qualname__)   # スタックトレースつき
                raise
            log.log(level, "終了 %s -> %r", func.__qualname__, result)
            return result
        return wrapper
    return decorator

@log_call()
def reserve(item, qty):
    if qty <= 0:
        raise ValueError("qty は1以上")
    return f"{item} を {qty} 個予約"

reserve("ホッチキス", 2)
try:
    reserve("付箋", 0)
except ValueError:
    pass
INFO __main__: 開始 reserve args=('ホッチキス', 2) kwargs={}
INFO __main__: 終了 reserve -> 'ホッチキス を 2 個予約'
INFO __main__: 開始 reserve args=('付箋', 0) kwargs={}
ERROR __main__: 例外 reserve
Traceback (most recent call last):
  (中略)
ValueError: qty は1以上

例外は記録したうえで raise で投げ直すので、呼び出し側の例外処理はそのまま使えます。ログに個人情報やパスワードが入る引数があるときは、引数を出さない設定(level を DEBUG にする等)にします。logging の設定は別記事「Python loggingの使い方」で詳しく扱っています。

レシピ5:引数を検証する

import functools, inspect

def validate_types(func):
    """型ヒントどおりの型かを実行時に確認する(単純な型のみ)"""
    sig = inspect.signature(func)
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        bound = sig.bind(*args, **kwargs)
        bound.apply_defaults()
        for name, value in bound.arguments.items():
            expected = sig.parameters[name].annotation
            if isinstance(expected, type) and not isinstance(value, expected):
                raise TypeError(f"{name} は {expected.__name__} が必要です(受け取った値: {value!r})")
        return func(*args, **kwargs)
    return wrapper

@validate_types
def order(item: str, qty: int) -> str:
    return f"{item} x {qty}"

print(order("付箋", 3))
try:
    order("付箋", "3")
except TypeError as e:
    print("TypeError:", e)
付箋 x 3
TypeError: qty は int が必要です(受け取った値: '3')

sig.bind() を使うと、位置引数でもキーワード引数でも「引数名→値」で扱えます。複雑な入力検証には Pydantic の @validate_call も選択肢になります。

レシピ6:権限をチェックする

import functools
from contextvars import ContextVar

current_user: ContextVar[dict] = ContextVar("current_user")

class PermissionDenied(Exception):
    pass

def require_role(*roles):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            user = current_user.get(None)
            if user is None or user["role"] not in roles:
                raise PermissionDenied(f"{func.__name__} には {roles} の権限が必要です")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@require_role("admin", "manager")
def delete_task(task_id):
    return f"タスク {task_id} を削除しました"

for user in [{"name": "佐藤", "role": "admin"}, {"name": "鈴木", "role": "member"}]:
    current_user.set(user)
    try:
        print(user["name"], "→", delete_task(42))
    except PermissionDenied as e:
        print(user["name"], "→ PermissionDenied:", e)
佐藤 → タスク 42 を削除しました
鈴木 → PermissionDenied: delete_task には ('admin', 'manager') の権限が必要です

ContextVar はスレッドや async タスクごとに値を分けて持てる変数です。 グローバル変数だと同時に動く処理どうしでユーザーが混ざるため、リクエスト単位の情報にはこちらが向いています。

レシピ7:非推奨の警告を出す

import functools, warnings

def deprecated(reason):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            warnings.warn(f"{func.__name__} は非推奨です: {reason}",
                          DeprecationWarning, stacklevel=2)   # 呼び出し元の行を指す
            return func(*args, **kwargs)
        return wrapper
    return decorator

@deprecated("get_items_v2() を使ってください")
def get_items():
    return ["棚", "机"]

warnings.simplefilter("always", DeprecationWarning)
print(get_items())
.../r07_deprecated.py:18: DeprecationWarning: get_items は非推奨です: get_items_v2() を使ってください
  print(get_items())
['棚', '机']

stacklevel=2 で、警告の場所が「呼び出した行」になります。 Python 3.13 以降は標準の @warnings.deprecated が使え、型チェッカーも呼び出し箇所を指摘してくれます。Python 3.14.7 で from warnings import deprecated → @deprecated("get_items_v2() を使ってください") と書き、同じ DeprecationWarning が出ることと、get_items.__deprecated__ に理由の文字列が入ることを確認しました。

レシピ8:タイムアウトを付ける

import asyncio, concurrent.futures, functools, time

_pool = concurrent.futures.ThreadPoolExecutor(max_workers=4)

def timeout(seconds):
    """同期関数:別スレッドで実行し、時間内に終わらなければ TimeoutError"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            future = _pool.submit(func, *args, **kwargs)
            try:
                return future.result(timeout=seconds)
            except concurrent.futures.TimeoutError:
                raise TimeoutError(f"{func.__name__} が {seconds}s 以内に終わりませんでした") from None
        return wrapper
    return decorator

def async_timeout(seconds):
    """async 関数:asyncio.timeout(3.11+)で本当に中断できる"""
    def decorator(func):
        @functools.wraps(func)
        async def wrapper(*args, **kwargs):
            async with asyncio.timeout(seconds):
                return await func(*args, **kwargs)
        return wrapper
    return decorator

@timeout(0.5)
def slow_export(sec):
    time.sleep(sec)
    return "出力完了"

print(slow_export(0.1))
try:
    slow_export(1.0)
except TimeoutError as e:
    print("TimeoutError:", e)

@async_timeout(0.5)
async def slow_fetch(sec):
    await asyncio.sleep(sec)
    return "取得完了"

async def main():
    print(await slow_fetch(0.1))
    try:
        await slow_fetch(1.0)
    except TimeoutError:
        print("TimeoutError: async 側は処理自体が中断されます")
asyncio.run(main())
_pool.shutdown(wait=True)
出力完了
TimeoutError: slow_export が 0.5s 以内に終わりませんでした
取得完了
TimeoutError: async 側は処理自体が中断されます

同期版は「呼び出し元が待つのをやめる」だけで、スレッド内の処理は最後まで動き続けます。 処理そのものを止めたい場合は、async 版(asyncio.timeout)か、requests の timeout= のように処理側の時間制限を使うのが確実です。asyncio の詳細は別記事「Python asyncioの使い方」で扱っています。

レシピ9:シングルトン(インスタンスを1つに)

import functools

def singleton(cls):
    instances = {}
    @functools.wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class Settings:
    def __init__(self, env="dev"):
        print(f"  Settings を初期化: {env}")
        self.env = env

a = Settings("prod")
b = Settings("dev")          # 2回目以降は初期化されない
print(a is b, b.env)
print(type(Settings))
  Settings を初期化: prod
True prod
<class 'function'>

最後の行のとおり、Settings はクラスではなく関数になります。 そのため isinstance(x, Settings) や継承は使えなくなります。それらが必要なら、モジュールの変数に1つだけ作る(settings = Settings())か、functools.cache を付けたファクトリ関数 get_settings() にすると素直です。

レシピ10:関数を登録する(プラグイン)

from collections.abc import Callable

EXPORTERS: dict[str, Callable[[list[dict]], str]] = {}

def exporter(fmt: str):
    """関数を名前つきで登録する(中身は変えずにそのまま返す)"""
    def decorator(func):
        if fmt in EXPORTERS:
            raise ValueError(f"{fmt} は登録済みです")
        EXPORTERS[fmt] = func
        return func                      # ラッパーを作らないので wraps も不要
    return decorator

@exporter("csv")
def to_csv(rows):
    head = ",".join(rows[0])
    return "\n".join([head] + [",".join(str(v) for v in r.values()) for r in rows])

@exporter("tsv")
def to_tsv(rows):
    return to_csv(rows).replace(",", "\t")

rows = [{"item": "棚", "qty": 2}, {"item": "机", "qty": 1}]
print(sorted(EXPORTERS))
print(EXPORTERS["csv"](rows))
['csv', 'tsv']
item,qty
棚,2
机,1

デコレータは「包む」だけでなく「登録して元の関数を返す」使い方もできます。 Flask の @app.route、pytest の @pytest.fixture、Click の @click.command などもこの考え方です。if 文の分岐を増やさずに、関数を足すだけで機能を増やせます。

レシピ11:同期・async の両方に対応する

import asyncio, functools, inspect, time

def timer(func):
    """同期関数にも async 関数にも使える計測デコレータ"""
    if inspect.iscoroutinefunction(func):
        @functools.wraps(func)
        async def async_wrapper(*args, **kwargs):
            start = time.perf_counter()
            try:
                return await func(*args, **kwargs)
            finally:
                print(f"[timer] {func.__name__}: {time.perf_counter() - start:.3f}s")
        return async_wrapper

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

@timer
async def fetch(name, sec):
    await asyncio.sleep(sec)
    return name

@timer
def calc():
    return sum(range(10**6))

async def main():
    print(await asyncio.gather(fetch("在庫", 0.2), fetch("注文", 0.3)))
asyncio.run(main())
calc()
print(inspect.iscoroutinefunction(fetch))
[timer] fetch: 0.200s
[timer] fetch: 0.301s
['在庫', '注文']
[timer] calc: 0.054s
True

async 関数に同期用の wrapper を付けると、計測されるのは「コルーチンを作る一瞬」だけになります。 inspect.iscoroutinefunction() で分岐し、async 用の wrapper では await してから後処理をします。判定には inspect 版を使います(asyncio.iscoroutinefunction は 3.14 で非推奨になりました)。

レシピ12:クラスデコレータでメソッドを加工する

import functools

def log_methods(cls):
    """クラスのすべての公開メソッドに呼び出しログを付けるクラスデコレータ"""
    for name, attr in list(vars(cls).items()):
        if callable(attr) and not name.startswith("_"):
            setattr(cls, name, _logged(attr))
    return cls                                  # クラスをそのまま返す

def _logged(func):
    @functools.wraps(func)
    def wrapper(self, *args, **kwargs):
        print(f"[log] {type(self).__name__}.{func.__name__}{args}")
        return func(self, *args, **kwargs)
    return wrapper

def add_to_dict(cls):
    """to_dict() メソッドを追加する"""
    cls.to_dict = lambda self: dict(vars(self))
    return cls

@add_to_dict
@log_methods
class Cart:
    def __init__(self):
        self.items = []
    def add(self, item):
        self.items.append(item)
    def count(self):
        return len(self.items)

cart = Cart()
cart.add("付箋"); cart.add("棚")
print(cart.count(), cart.to_dict())
print(Cart.add.__name__, type(Cart))
[log] Cart.add('付箋',)
[log] Cart.add('棚',)
[log] Cart.count()
2 {'items': ['付箋', '棚']}
add <class 'type'>

クラスデコレータはクラスを受け取り、加工したクラスを返します。 レシピ9と違ってクラスそのものを返すので、type(Cart) は type のままで、継承や isinstance もそのまま使えます。@dataclass もこの仕組みで __init__ や __repr__ を追加しています。

引数付きデコレータ・クラス型・重ねがけ

引数付きデコレータは3重の関数

@retry(times=3) は「retry(times=3) を呼んだ戻り値(=デコレータ)を適用する」という意味です。 そのため関数が1段増えます。

段 受け取るもの 返すもの
1段目 retry(times) デコレータの引数 decorator
2段目 decorator(func) 元の関数 wrapper
3段目 wrapper(*args, **kwargs) 呼び出し時の引数 戻り値

@timer と @timer(unit="s") の両方の書き方に対応させるには、最初の引数を func=None にして分岐します。

import functools, time

def timer(func=None, *, unit="ms"):
    """@timer と @timer(unit="s") のどちらでも使える"""
    def decorator(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            t = time.perf_counter()
            r = f(*args, **kwargs)
            d = time.perf_counter() - t
            print(f"[{f.__name__}] {d*1000:.1f}ms" if unit == "ms" else f"[{f.__name__}] {d:.4f}s")
            return r
        return wrapper
    if func is None:          # @timer(unit="s") の形
        return decorator
    return decorator(func)    # @timer の形

@timer
def a(): return sum(range(10**5))
@timer(unit="s")
def b(): return sum(range(10**5))
a(); b()
[a] 1.0ms
[b] 0.0013s

クラスで書くデコレータ

呼び出し回数のような「状態」を持たせたいときは、__call__ を持つクラスで書けます。 クラスでは wraps の代わりに functools.update_wrapper(self, func) を使います。メソッドに付ける場合は __get__ を定義して self を渡せるようにします(Inventory クラスのメソッドに付けて、2回の呼び出しが数えられることを確認しました)。

import functools

class CountCalls:
    """呼び出し回数を数えるクラス型デコレータ"""
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.calls = 0
    def __call__(self, *args, **kwargs):
        self.calls += 1
        return self.func(*args, **kwargs)
    def __get__(self, obj, objtype=None):     # メソッドに付けたとき self を渡すため
        if obj is None:
            return self
        return functools.partial(self.__call__, obj)

@CountCalls
def ping():
    return "pong"

ping(); ping()
print(ping.__name__, ping.calls)
ping 2

引数付きをクラスで書くときは、__init__ でデコレータの引数、__call__ で関数を受け取ります(@Limit(2) のような形)。

重ねがけの順序

適用は下(関数に近い方)から、呼び出し時の前処理は上からです。

import functools
def tag(name):
    def decorator(func):
        print(f"適用: {name}")                     # デコレート時(定義時)に1回
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"  {name} 前")
            result = func(*args, **kwargs)
            print(f"  {name} 後")
            return result
        return wrapper
    return decorator

@tag("A")
@tag("B")
def hello():
    print("  本体")
# hello = tag("A")(tag("B")(hello)) と同じ
print("--- 呼び出し")
hello()
適用: B
適用: A
--- 呼び出し
  A 前
  B 前
  本体
  B 後
  A 後
組み合わせ 置く順番(上→下) 理由
権限チェック+キャッシュ @require_role → @cache 権限の無い人にもキャッシュ結果が返るのを避ける
計測+リトライ @timer → @retry 再試行も含めた合計時間を測る(逆なら1回ごと)
@classmethod・@staticmethod・@property と自作 標準のものを一番上 自作デコレータは普通の関数を受け取る前提のため
ルート登録(@app.get 等)と自作 登録を一番上 加工済みの関数を登録するため

標準ライブラリのデコレータ一覧

デコレータ モジュール 用途
@property 組み込み メソッドを属性のように読む
@classmethod 組み込み クラスを第1引数に受け取る(別名コンストラクタ等)
@staticmethod 組み込み self も cls も受け取らない
@functools.wraps functools wrapper に元の関数の情報を写す
@functools.cache functools 上限なしのメモ化(3.9+)
@functools.lru_cache functools 上限つきのメモ化
@functools.cached_property functools 初回だけ計算する属性
@functools.singledispatch functools 第1引数の型で処理を切り替える
@functools.total_ordering functools __eq__ と1つの比較から残りの比較を補う
@dataclasses.dataclass dataclasses __init__ や __repr__ を自動生成
@contextlib.contextmanager contextlib yield 1つで with 文用の関数を作る
@abc.abstractmethod abc 子クラスで実装必須のメソッド
@atexit.register atexit 終了時に呼ぶ関数を登録
@typing.overload typing 引数の型ごとの戻り値を型チェッカーに伝える
@typing.final typing 継承・オーバーライドの禁止を伝える
@typing.override typing 親のメソッドを上書きしていることを示す(3.12+)
@warnings.deprecated warnings 非推奨を示す(3.13+)

dataclass の詳しい使い方は別記事「Python dataclassの使い方」にまとめています。

よくあるエラーと直し方

次の5つは、デコレータを書き始めたときによく出会うものです。Python 3.11.15 での実際のエラー文を載せます。

症状・エラー文 原因 直し方
関数の戻り値が None になる wrapper で return を書いていない return func(*args, **kwargs)
TypeError: retry.<locals>.decorator() missing 1 required positional argument: 'func' 引数付きデコレータを @retry と () なしで使った @retry(3) のように呼ぶ/func=None 方式で両対応
TypeError: timer() missing 1 required positional argument: 'func' 引数なしデコレータに @timer() と () を付けた @timer にする
TypeError: simple.<locals>.wrapper() takes 0 positional arguments but 2 were given wrapper が引数を受け取っていない def wrapper(*args, **kwargs)
呼んでいないのに関数が実行される デコレータ本体(wrapper の外)で func() を呼んでいる 処理は wrapper の中に書く
ログやドキュメントの関数名が wrapper になる wraps を付けていない @functools.wraps(func)
TypeError: unhashable type: 'list' キャッシュの引数にリストを渡した タプルに変換して渡す
メソッドに付けたクラス型デコレータで self が渡らない __get__ が無い __get__ で functools.partial(self.__call__, obj) を返す

「デコレータ本体は定義時に1回、wrapper は呼び出しのたびに実行される」と区別しておくと、5番目のような思いがけない実行を防げます。

よくある質問

Q. デコレータはどんなときに使うとよいですか? A. ログ・計測・リトライ・権限チェック・キャッシュのように、「多くの関数に同じ前後処理を足したい」ときに向いています。1つの関数にしか使わない処理なら、関数の中に直接書く方が読みやすいことも多いです。

Q. functools.wraps は必ず付けるべきですか? A. wrapper を作るデコレータでは付けるのが基本です。付けないと関数名・docstring・シグネチャが wrapper のものになり、ログ・help()・ドキュメント生成・フレームワークの引数解析に影響します。レシピ10のように元の関数をそのまま返すデコレータでは不要です。

Q. デコレータを付けた関数を、デコレータなしでテストできますか? A. wraps を付けていれば func.__wrapped__ で元の関数を呼べます。リトライや待機を飛ばして中身だけを確かめたいテストで便利です。

Q. 関数デコレータとクラスデコレータの違いは何ですか? A. 受け取るものが関数かクラスかの違いです。関数デコレータは関数を包んで返し、クラスデコレータはクラスにメソッドや属性を足して返します(レシピ12)。どちらも仕組みは「受け取って返す」で同じです。

まとめ

  • デコレータは関数を受け取り関数を返す関数で、@deco は func = deco(func) の糖衣構文
  • wrapper は *args, **kwargs で受けて、戻り値を必ず return する
  • @functools.wraps で名前・docstring・シグネチャを保つ。型まで保つなら ParamSpec(3.12+ は def deco[**P, R])
  • 引数付きは3重の関数、func=None で @deco と @deco(...) の両対応にできる
  • 重ねがけは下から適用、上から前処理。権限チェックはキャッシュより外側に置く
  • async 関数には async の wrapper を用意し、inspect.iscoroutinefunction() で分岐する
  • キャッシュは functools.cache / lru_cache、非推奨は 3.13+ の warnings.deprecated など標準ライブラリを先に検討する

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Pythonデコレータの使い方|コピペで使えるレシピ12選」

株式会社RJCでは、Pythonの読みやすく再利用しやすい書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?