閉じる

Pythonのデコレータの使い方入門: 関数を変更せず機能を拡張する

関数やクラスのソースを直接変更せずに、ログ出力や実行時間計測、入力チェックなどの機能を後付けできるのがデコレータです。

本記事では、初心者の方にもわかるように、@記法の基本から実践的なユースケース、メソッドやクラスへの適用まで段階的に解説します。

PEP8に沿った書き方や型ヒントも取り入れます。

初心者向け: Pythonデコレータの基本

デコレータとは 関数を変更せず機能を拡張

デコレータは、関数(やメソッド、クラス)を引数に受け取り、拡張した新しい関数(やクラス)を返す高階関数です。

元の関数の振る舞いをラップ(wrapper)して、前後に処理を挿入したり、例外処理やログを追加したりできます。

これにより、関心の分離を保ったまま再利用可能な横断的関心事(ロギングや認可など)を実現できます。

@記法と高階関数の仕組み

Pythonではデコレータを@decorator_nameというシンタックスシュガーで適用します。

実体は「関数を関数に渡して戻り値で置き換える」操作です。

Python
# デコレータの最小例
from typing import Callable

def hello_decorator(func: Callable) -> Callable:
    def wrapper():
        print("前処理: Hello")
        result = func()
        print("後処理: Bye")
        return result
    return wrapper

# @記法の利用
@hello_decorator
def greet():
    print("中身: greetが呼ばれました")

# 上の@は次と等価です:
# greet = hello_decorator(greet)

greet()
実行結果
前処理: Hello
中身: greetが呼ばれました
後処理: Bye

はじめてのデコレータの使い方

最初は引数も戻り値もない関数で感覚を掴むのが安全です。

PEP8に従って、関数名はsnake_caseで書き、内側の関数名はwrapperなど意味が伝わる名前にします。

Python
def notify(func):
    """呼び出しの前後でメッセージを表示する簡単なデコレータ"""
    def wrapper():
        print("[notify] start")
        value = func()
        print("[notify] end")
        return value
    return wrapper

@notify
def say():
    print("Hi!")

say()
実行結果
[notify] start
Hi!
[notify] end

デコレータの実装とベストプラクティス

wrapperと*args **kwargsの受け渡し

実務では引数や戻り値がある関数をラップする必要があります。

そのため、*args**kwargsであらゆる引数を受け取り、元の関数にそのまま渡します。

Python
from typing import Any, Callable

def debug_args(func: Callable[..., Any]) -> Callable[..., Any]:
    """引数と戻り値を表示するデコレータ"""
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        print(f"[debug_args] args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"[debug_args] result={result!r}")
        return result
    return wrapper

@debug_args
def add(a: int, b: int, scale: float = 1.0) -> float:
    return (a + b) * scale

add(2, 3, scale=0.5)
実行結果
[debug_args] args=(2, 3), kwargs={'scale': 0.5}
[debug_args] result=2.5

functools.wrapsでメタデータを保持

デコレータを適用すると元の関数名や__doc__が失われがちです。

functools.wrapsでメタデータをコピーしましょう。

ドキュメント生成やデバッグ、型チェッカーとの相性が良くなります。

Python
from functools import wraps
from typing import TypeVar, Callable, Any

F = TypeVar("F", bound=Callable[..., Any])

def preserve_meta(func: F) -> F:
    """wrapsを使ってメタデータを保持する例"""
    @wraps(func)  # これが重要です
    def wrapper(*args: Any, **kwargs: Any):
        """内側のdocstringは外に見えなくなる(元のdocstringが保持される)"""
        return func(*args, **kwargs)
    return wrapper  # type: ignore[return-value]

@preserve_meta
def area(radius: float) -> float:
    """半径から円の面積を計算します"""
    from math import pi
    return pi * radius * radius

print(area.__name__)   # 元の関数名が保たれる
print(area.__doc__)    # 元のdocstringが保たれる
実行結果
area
半径から円の面積を計算します

引数ありデコレータの作り方

デコレータ自身に設定値を渡したいことがあります。

その場合は「関数を返す関数を返す」三層構造になります。

Python
from functools import wraps
from typing import Callable, Any

def repeat(times: int = 2) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    """指定回数だけ関数を繰り返し実行するデコレータファクトリ"""
    def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            result = None
            for i in range(times):
                print(f"[repeat] {i+1}/{times}")
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def ping() -> str:
    print("pong")
    return "ok"

ping()
実行結果
[repeat] 1/3
pong
[repeat] 2/3
pong
[repeat] 3/3
pong

複数デコレータの適用順と注意点

デコレータは「下から先に適用」されます。

ソース上でdefに近いものが最初に関数へ作用し、その戻り値に対して次のデコレータが適用されます。

順序が挙動に影響するため、ログとキャッシュなどを組み合わせる場合は意図を明確にしましょう。

Python
def A(func):
    def wrapper(*a, **k):
        print("A: before")
        r = func(*a, **k)
        print("A: after")
        return r
    return wrapper

def B(func):
    def wrapper(*a, **k):
        print("B: before")
        r = func(*a, **k)
        print("B: after")
        return r
    return wrapper

@A
@B
def work():
    print("do work")

work()
実行結果
A: before
B: before
do work
B: after
A: after

上の例では@Bが先にworkへ適用され、次に@Aが適用されています。

例外処理と戻り値の扱い

デコレータ側で例外を握りつぶすと、呼び出し元でバグを検知できなくなります。

原則は「ログして再送出(raise)」か「仕様として明確にデフォルトを返す」です。

Python
from functools import wraps
from typing import Any, Callable, Optional, Type, Tuple

def safe(default: Optional[Any] = None,
         suppress: Tuple[Type[BaseException], ...] = (Exception,)) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    """
    例外をログし、指定の例外のみ抑制してdefaultを返す。
    それ以外は再送出します。
    """
    def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            try:
                return func(*args, **kwargs)
            except suppress as e:
                print(f"[safe] suppressed: {e!r}")
                return default
        return wrapper
    return decorator

@safe(default=-1, suppress=(ZeroDivisionError,))
def div(a: int, b: int) -> float:
    return a / b

print(div(4, 2))  # 正常
print(div(1, 0))  # 例外を抑制しdefaultを返す
実行結果
2.0
[safe] suppressed: ZeroDivisionError('division by zero')
-1

デコレータのユースケース

ログ出力と実行時間の計測

関数の開始と終了、実行時間を測るのは定番です。

time.perf_counter()で高精度に計測します。

Python
import time
from functools import wraps
from typing import Any, Callable

def timeit(label: str | None = None) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    """実行時間を計測して表示するデコレータ"""
    def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            name = label or func.__name__
            start = time.perf_counter()
            try:
                return func(*args, **kwargs)
            finally:
                dur = (time.perf_counter() - start) * 1000
                print(f"[timeit] {name}: {dur:.2f} ms")
        return wrapper
    return decorator

@timeit()
def slow(n: int) -> int:
    time.sleep(0.05)
    return n * 2

print(slow(5))
実行結果
[timeit] slow: 50.?? ms
10

(実行時間は環境により変動します)

権限チェックと入力バリデーション

認可と入力チェックは関数の外側に置くと見通しが良くなります。

Python
from dataclasses import dataclass
from functools import wraps
from typing import Callable, Any

@dataclass
class User:
    name: str
    is_admin: bool = False

def requires_admin(func: Callable[..., Any]) -> Callable[..., Any]:
    """第1引数userの権限をチェックするデコレータ"""
    @wraps(func)
    def wrapper(user: User, *args: Any, **kwargs: Any) -> Any:
        if not user.is_admin:
            raise PermissionError("admin権限が必要です")
        return func(user, *args, **kwargs)
    return wrapper

def validate_positive(func: Callable[..., Any]) -> Callable[..., Any]:
    """数値引数が正であることをバリデートする例"""
    @wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        for x in args:
            if isinstance(x, (int, float)) and x <= 0:
                raise ValueError("引数は正の数である必要があります")
        return func(*args, **kwargs)
    return wrapper

@requires_admin
@validate_positive
def create_quota(user: User, size_gb: int) -> str:
    return f"{user.name} に {size_gb}GB のクォータを作成しました"

admin = User("Alice", is_admin=True)
guest = User("Bob", is_admin=False)

print(create_quota(admin, 10))
# print(create_quota(guest, 10))  # PermissionError
実行結果
Alice に 10GB のクォータを作成しました

キャッシュやリトライの導入方針

計算コストが高い処理は結果をキャッシュし、外部I/Oは一時的失敗をリトライすると安定します。

標準のfunctools.lru_cacheで簡単にメモ化できます。

Python
from functools import lru_cache

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

print(fib(30))  # 高速に計算される(以降はキャッシュ)
実行結果
832040

リトライはパラメータ化されたデコレータで柔軟に実装できます。

Python
import time
from functools import wraps
from typing import Callable, Any, Tuple, Type

def retry(retries: int = 3, delay: float = 0.1,
          exceptions: Tuple[Type[BaseException], ...] = (Exception,)) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    """一時的な失敗をリトライするデコレータ"""
    def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
        @wraps(func)
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            last = None
            for attempt in range(1, retries + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last = e
                    print(f"[retry] attempt {attempt}/{retries} failed: {e!r}")
                    if attempt < retries:
                        time.sleep(delay)
            assert last is not None
            raise last
        return wrapper
    return decorator

count = {"n": 0}

@retry(retries=3, delay=0.05, exceptions=(RuntimeError,))
def flaky() -> str:
    count["n"] += 1
    if count["n"] < 3:
        raise RuntimeError("temporary failure")
    return "success"

print(flaky())
実行結果
[retry] attempt 1/3 failed: RuntimeError('temporary failure')
[retry] attempt 2/3 failed: RuntimeError('temporary failure')
success

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

メソッドにデコレータを適用するポイント

インスタンスメソッドでもselfは通常の第1引数として渡されます。

*args**kwargsで受けてそのまま渡せば問題ありません。

Python
from functools import wraps
from typing import Any, Callable

def trace(func: Callable[..., Any]) -> Callable[..., Any]:
    """呼び出しをトレースするデコレータ"""
    @wraps(func)
    def wrapper(*args: Any, **kwargs: Any) -> Any:
        print(f"[trace] {func.__qualname__} called")
        return func(*args, **kwargs)
    return wrapper

class Calculator:
    @trace
    def mul(self, a: int, b: int) -> int:
        return a * b

c = Calculator()
print(c.mul(3, 4))
実行結果
[trace] Calculator.mul called
12

@staticmethodと@classmethodの使い分け

  • @staticmethodselfclsを受け取らない関数をクラスの名前空間にまとめたいときに使います。
  • @classmethodはクラス自身を第1引数clsで受け取り、オルタナティブコンストラクタやクラス全体に関する処理に使います。

これらに独自デコレータを併用する場合、基本的には「独自デコレータをdefに近く、その上に@staticmethod@classmethod」の順が安全です。

こうすると独自デコレータが素の関数をラップし、最後にメソッド化されます。

Python
from functools import wraps
from typing import Any, Callable

def tag(name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    def deco(func: Callable[..., Any]) -> Callable[..., Any]:
        @wraps(func)
        def wrapper(*a: Any, **k: Any) -> Any:
            print(f"[{name}] {func.__name__}")
            return func(*a, **k)
        return wrapper
    return deco

class Util:
    @staticmethod        # 外側: メソッド化
    @tag("static")       # 内側: 素の関数を装飾
    def identity(x: int) -> int:
        return x

    @classmethod         # 外側: メソッド化
    @tag("class")        # 内側: 素の関数を装飾
    def from_str(cls, s: str) -> tuple[type, str]:
        return (cls, s)

print(Util.identity(7))
print(Util.from_str("ok"))
実行結果
[static] identity
7
[class] from_str
(<class '__main__.Util'>, 'ok')

クラスデコレータでクラスに機能追加

クラス全体を受け取り、属性を追加したり登録処理を行うのがクラスデコレータです。

軽量なメタクラスの代替として有用です。

Python
from functools import wraps
from typing import Type, Any, Dict

def auto_repr(cls: Type[Any]) -> Type[Any]:
    """
    __repr__が未定義なら、自動でフィールドから生成する。
    PEP8に沿い、クラスはCapWords、関数はsnake_caseで命名します。
    """
    if "__repr__" in cls.__dict__:
        return cls

    def __repr__(self) -> str:
        fields = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
        return f"{cls.__name__}({fields})"

    setattr(cls, "__repr__", __repr__)
    return cls

REGISTRY: Dict[str, Type[Any]] = {}

def register(name: str):
    """クラスをレジストリに登録するクラスデコレータ"""
    def decorator(cls: Type[Any]) -> Type[Any]:
        REGISTRY[name] = cls
        return cls
    return decorator

@register("point")
@auto_repr
class Point:
    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y

p = Point(2, 3)
print(p)                 # 自動生成されたrepr
print(REGISTRY["point"]) # レジストリに登録済み
実行結果
Point(x=2, y=3)
<class '__main__.Point'>

まとめ

デコレータは「関数やクラスを引数に受けて拡張したものを返す」高階関数であり、既存コードを変更せずに機能を追加できる強力な仕組みです。

基本は*args**kwargsで汎用ラッパーを作り、functools.wrapsでメタデータを保護することです。

複数デコレータの適用順や、例外の扱い(再送出かデフォルト返却か)を明確にし、ログ出力、実行時間計測、認可、バリデーション、キャッシュ、リトライといった横断的関心事を整理して適用します。

メソッドでは@staticmethod@classmethodとの組み合わせ順に注意し、クラスデコレータで振る舞いを横断的に付与する設計も有効です。

PEP8に沿った命名と型ヒントを添えることで、可読性と保守性がさらに高まります。

まずは小さな関数から装飾を試し、ユースケースに合わせて少しずつ抽象度を上げていくと着実に使いこなせます。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!