基礎

Pythonデコレータ入門|関数を拡張する強力な仕組み

Python デコレータ 関数

Pythonデコレータ入門
関数を拡張する強力な仕組み

デコレータを使えば、既存の関数に機能を追加したり、共通処理をまとめたりできます。基本の仕組みから実践的な活用パターンまでステップごとに解説します。

こんな人向けの記事です

  • Pythonの@構文の意味を知りたい
  • デコレータの仕組みを基礎から理解したい
  • 実務で使える実践的なデコレータパターンを学びたい

Step 1デコレータとは何か

デコレータとは、既存の関数やクラスを変更せずに、機能を追加・拡張する仕組みです。Pythonでは関数が「第一級オブジェクト」であること(変数に代入したり、他の関数に渡したりできること)を活用しています。

デコレータの本質

デコレータは「関数を受け取って、新しい関数を返す関数」です。数学的に書くと decorator(func) → new_func というイメージです。

まずはデコレータを使わない形で、関数を別の関数で「包む」基本パターンを見てみましょう。

Python
# デコレータの仕組みを理解するための基本例
def greet():
    print("こんにちは!")

def add_greeting(func):
    def wrapper():
        print("--- 挨拶開始 ---")
        func()
        print("--- 挨拶終了 ---")
    return wrapper

# 関数を「包む」
greet = add_greeting(greet)
greet()
# --- 挨拶開始 ---
# こんにちは!
# --- 挨拶終了 ---

この add_greeting(greet) の部分こそがデコレータの本質です。元の関数 greet を変更せずに、前後に処理を追加できました。

Step 2関数デコレータの基本(@構文)

Step 1で見た greet = add_greeting(greet) という書き方を、Pythonでは @ 構文でシンプルに書けます。

Python
def add_greeting(func):
    def wrapper():
        print("--- 挨拶開始 ---")
        func()
        print("--- 挨拶終了 ---")
    return wrapper

@add_greeting   # greet = add_greeting(greet) と同じ意味
def greet():
    print("こんにちは!")

greet()
# --- 挨拶開始 ---
# こんにちは!
# --- 挨拶終了 ---

@add_greeting を関数定義の直前に書くだけで、自動的に関数がラップされます。これがPythonのデコレータ構文です。

次に、引数を持つ関数にも対応できるようにしましょう。*args**kwargs を使います。

Python
def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"関数 {func.__name__} が呼ばれました")
        print(f"  引数: {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"  戻り値: {result}")
        return result
    return wrapper

@log_call
def add(a, b):
    return a + b

result = add(3, 5)
# 関数 add が呼ばれました
#   引数: (3, 5), {}
#   戻り値: 8
よくあるミス

wrapper関数で return func(*args, **kwargs)return を忘れると、デコレートされた関数の戻り値が常に None になります。必ず戻り値を返しましょう。

Step 3引数付きデコレータ

デコレータ自体に引数を渡したい場合は、「デコレータを返す関数」を作ります。つまり関数を3重にネストします。

Python
def repeat(n):
    """関数をn回繰り返すデコレータ"""
    def decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)   # repeat(3) が実行され、decorator が返される → @decorator
def say_hello():
    print("Hello!")

say_hello()
# Hello!
# Hello!
# Hello!
3重ネストの理解

@repeat(3) は2段階で処理されます。まず repeat(3) が実行されて decorator 関数が返り、次に @decoratorsay_hello に適用されます。

実用的な例として、リトライ処理のデコレータを見てみましょう。

Python
import time

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

@retry(max_attempts=3, delay=2)
def fetch_data(url):
    # ネットワーク通信など失敗する可能性のある処理
    import requests
    return requests.get(url)

Step 4functools.wrapsの重要性

デコレータを使うと、元の関数の情報(名前やドキュメント文字列)が失われてしまう問題があります。

Python
def my_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet():
    """挨拶する関数"""
    print("こんにちは")

# 元の関数の情報が失われている!
print(greet.__name__)    # "wrapper"("greet"ではない)
print(greet.__doc__)     # None("挨拶する関数"ではない)
なぜ問題なのか

__name____doc__ が正しくないと、デバッグ時のトレースバックが読みにくくなり、help() 関数も正しい情報を返さなくなります。また、Djangoのビューデコレータなど、関数名に依存する処理が正常に動かなくなることもあります。

この問題を解決するのが functools.wraps です。

Python
from functools import wraps

def my_decorator(func):
    @wraps(func)   # ← これを追加するだけ!
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet():
    """挨拶する関数"""
    print("こんにちは")

# 元の関数の情報が保持される
print(greet.__name__)    # "greet" ✓
print(greet.__doc__)     # "挨拶する関数" ✓
ベストプラクティス

デコレータを書くときは常に @functools.wraps(func) を付けるのがベストプラクティスです。たった1行追加するだけでデバッグ時の混乱を防げます。

Step 5クラスデコレータ

Pythonでは、__call__ メソッドを持つクラスもデコレータとして使えます。状態を保持したい場合に便利です。

Python
from functools import wraps

class CountCalls:
    """関数の呼び出し回数をカウントするデコレータ"""

    def __init__(self, func):
        wraps(func)(self)  # functools.wrapsをクラスで使う方法
        self.func = func
        self.call_count = 0

    def __call__(self, *args, **kwargs):
        self.call_count += 1
        print(f"{self.func.__name__} の呼び出し: {self.call_count}回目")
        return self.func(*args, **kwargs)

@CountCalls
def say_hello(name):
    print(f"Hello, {name}!")

say_hello("太郎")  # say_hello の呼び出し: 1回目 → Hello, 太郎!
say_hello("花子")  # say_hello の呼び出し: 2回目 → Hello, 花子!
print(say_hello.call_count)  # 2

クラスデコレータを使えば、クラスそのものを拡張することもできます。

Python
def add_repr(cls):
    """__repr__メソッドを自動追加するクラスデコレータ"""
    def __repr__(self):
        attrs = ", ".join(
            f"{k}={v!r}" for k, v in self.__dict__.items()
        )
        return f"{cls.__name__}({attrs})"

    cls.__repr__ = __repr__
    return cls

@add_repr
class User:
    def __init__(self, name, age):
        self.name = name
        self.age = age

user = User("太郎", 25)
print(user)  # User(name='太郎', age=25)
Python標準のクラスデコレータ

Python 3.7以降の @dataclasses.dataclass はクラスデコレータの代表例です。__init____repr____eq__ などを自動生成してくれます。

Step 6実践的なデコレータ例

ここでは実務でよく使われる3つのデコレータパターンを紹介します。

1. ログ出力デコレータ

Python
import logging
from functools import wraps

def log_function(logger=None):
    """関数の呼び出しと結果をログ出力するデコレータ"""
    if logger is None:
        logger = logging.getLogger(__name__)

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            logger.info(f"開始: {func.__name__}(args={args}, kwargs={kwargs})")
            try:
                result = func(*args, **kwargs)
                logger.info(f"完了: {func.__name__} → {result}")
                return result
            except Exception as e:
                logger.error(f"エラー: {func.__name__} → {e}")
                raise
        return wrapper
    return decorator

@log_function()
def calculate_total(items):
    return sum(item["price"] for item in items)

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

Python
import time
from functools import wraps

def measure_time(func):
    """関数の実行時間を計測するデコレータ"""
    @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

@measure_time
def heavy_process():
    """重い処理のシミュレーション"""
    total = sum(i * i for i in range(10_000_000))
    return total

heavy_process()
# heavy_process: 0.8523秒

3. 認証チェックデコレータ(Django風)

Python
from functools import wraps

def require_role(role):
    """指定された権限を持つユーザーのみアクセスを許可するデコレータ"""
    def decorator(func):
        @wraps(func)
        def wrapper(request, *args, **kwargs):
            if not request.user.is_authenticated:
                return redirect("/login/")
            if role not in request.user.roles:
                return HttpResponseForbidden("権限がありません")
            return func(request, *args, **kwargs)
        return wrapper
    return decorator

# Django のビュー関数での使用例
@require_role("admin")
def admin_dashboard(request):
    return render(request, "admin/dashboard.html")
Djangoの組み込みデコレータ

Djangoには @login_required@permission_required@csrf_exempt など多くの組み込みデコレータがあります。これらの内部実装も同じ仕組みです。

まとめチェックリスト

  • デコレータは「関数を受け取って新しい関数を返す関数」
  • @構文func = decorator(func) のシンタックスシュガー
  • 引数付きデコレータは3重ネスト(デコレータファクトリ)で作る
  • @functools.wraps(func) は常に付けるのがベストプラクティス
  • __call__ を持つクラスもデコレータとして使える
  • ログ出力・時間計測・認証チェックなど実務で活躍する場面は多い