プログラミング

Pythonのデコレータとは?使い方をわかりやすく解説!(decorator:関数の拡張:@記法:ラッパー関数:functools.wrapsなど)Pythonのデコレータとは?使い方をわかりやすく解説!(decorator:関数の拡張:@記法:ラッパー関数:functools.wrapsなど)

当サイトでは記事内に広告を含みます

Pythonを学んでいると「デコレータ」という機能に出会うことがあります。ログの記録・実行時間の計測・認証チェックなど、既存の関数に共通の処理を追加したい場面でデコレータは非常に役立つPythonの中級機能のひとつです。

デコレータは関数を引数として受け取り、元の関数に処理を追加した新しい関数を返す仕組みです。@記法を使うと関数定義の直前にデコレータを指定でき、コードの重複を大幅に減らせます。functools.wrapsを使うことでデコレータを適用した後も元の関数の情報を保持できます。

この記事では、Pythonのデコレータの仕組みと書き方を、基本的な@記法・引数付きデコレータ・functools.wraps・実践的な応用パターンまで、サンプルコードとともにわかりやすく解説していきます。

Pythonのデコレータとは関数を受け取って機能を追加した関数を返す仕組み

それではまず、デコレータの基本的な仕組みと@記法の使い方について解説していきます。

デコレータを理解するには、まずPythonでは関数も変数と同様にオブジェクトとして扱えるため、関数を引数に渡したり戻り値として返したりできるという点を押さえておくことが大切です。


# デコレータの基本的な仕組み
def my_decorator(func):
    """デコレータ関数:funcを受け取りwrapperを返す"""
    def wrapper():
        print("【前処理】関数を実行します")
        func()   # 元の関数を呼び出す
        print("【後処理】関数が完了しました")
    return wrapper

# @記法を使ったデコレータの適用
@my_decorator
def say_hello():
    print("こんにちは!アボカドとサーモンです。")

# デコレータを適用した関数を呼び出す
say_hello()

# 出力結果:【前処理】関数を実行します
# 出力結果:こんにちは!アボカドとサーモンです。
# 出力結果:【後処理】関数が完了しました

@my_decoratorは「say_hello = my_decorator(say_hello)」と書くのと同じ意味です。@記法を使うことで、デコレータの適用が関数定義と同じ場所に書けてコードが読みやすくなります。

デコレータは「元の関数を変更せずに機能を追加する」という設計思想に基づいています。ログ・認証・キャッシュ・バリデーションなど横断的な関心事(クロスカッティングコンサーン)を関数本体から分離して書けるのが大きなメリットです。

デコレータを@記法なしで書いた場合との比較


def log_decorator(func):
    def wrapper():
        print(f"「{func.__name__}」を実行します")
        func()
        print(f"「{func.__name__}」が完了しました")
    return wrapper

# @記法なし(手動で適用)
def greet_gorilla():
    print("ゴリラにご挨拶")

greet_gorilla = log_decorator(greet_gorilla)   # 手動で適用
greet_gorilla()

print("---")

# @記法あり(同じ動作)
@log_decorator
def greet_avocado():
    print("アボカドにご挨拶")

greet_avocado()

# 出力結果:「greet_gorilla」を実行します
# 出力結果:ゴリラにご挨拶
# 出力結果:「greet_gorilla」が完了しました
# 出力結果:---
# 出力結果:「greet_avocado」を実行します
# 出力結果:アボカドにご挨拶
# 出力結果:「greet_avocado」が完了しました

@記法は手動での適用を糖衣構文(シンタックスシュガー)として簡略化したものです。どちらも同じ動作をしますが、@記法の方が意図が明確でコードもスッキリします。

引数を持つ関数へのデコレータの適用


# 引数と戻り値を持つ関数にデコレータを適用する
def log_decorator(func):
    def wrapper(*args, **kwargs):   # 任意の引数を受け取れるように
        print(f"関数「{func.__name__}」を実行:引数={args}, {kwargs}")
        result = func(*args, **kwargs)   # 元の関数を実行して結果を取得
        print(f"戻り値:{result}")
        return result   # 戻り値を返す
    return wrapper

@log_decorator
def calculate_total(price, quantity, discount=0):
    return price * quantity * (1 - discount)

total = calculate_total(298, 50, discount=0.1)
print(f"合計金額:{total:.0f}円")

# 出力結果:関数「calculate_total」を実行:引数=(298, 50), {'discount': 0.1}
# 出力結果:戻り値:13410.0
# 出力結果:合計金額:13410円

wrapper関数で*argsと**kwargsを使うことで、任意の引数を持つ関数に対しても汎用的に使えるデコレータが書けます。実務で使うデコレータは必ずこの形にしておきましょう。

functools.wrapsでデコレータの情報を保持する方法

続いては、functools.wrapsを使ってデコレータを適用した後も元の関数の情報を保持する方法を確認していきます。

デコレータを適用すると元の関数名やdocstringが失われてしまいます。functools.wrapsを使うとwrapper関数に元の関数の__name__や__doc__などの情報をコピーして保持できます。

functools.wrapsなしとありの違い


import functools

# wrapsなし
def decorator_no_wraps(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@decorator_no_wraps
def greet(name):
    """挨拶する関数"""
    return f"こんにちは、{name}さん!"

print(f"wrapsなし:{greet.__name__}")
print(f"wrapsなし:{greet.__doc__}")

# wrapsあり
def decorator_with_wraps(func):
    @functools.wraps(func)   # 元の関数の情報を保持
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@decorator_with_wraps
def greet2(name):
    """挨拶する関数"""
    return f"こんにちは、{name}さん!"

print(f"wrapsあり:{greet2.__name__}")
print(f"wrapsあり:{greet2.__doc__}")

# 出力結果:wrapsなし:wrapper
# 出力結果:wrapsなし:None
# 出力結果:wrapsあり:greet2
# 出力結果:wrapsあり:挨拶する関数

functools.wrapsなしでは関数名がwrapperに置き換わりdocstringも失われます。デバッグやドキュメント生成に支障が出るため、デコレータを書く際はfunctools.wrapsを必ず付けましょう。

デコレータの基本テンプレート


import functools

# デコレータの推奨テンプレート
def my_decorator(func):
    @functools.wraps(func)   # 必ず付ける
    def wrapper(*args, **kwargs):
        # 前処理
        result = func(*args, **kwargs)
        # 後処理
        return result
    return wrapper

@my_decorator
def sample_function(name, price=100):
    """サンプル関数のdocstring"""
    return f"{name}:{price}円"

print(sample_function("アボカド", price=298))
print(f"関数名:{sample_function.__name__}")
print(f"docstring:{sample_function.__doc__}")

# 出力結果:アボカド:298円
# 出力結果:関数名:sample_function
# 出力結果:docstring:サンプル関数のdocstring

このテンプレートを覚えておくことで、どんなデコレータを書くときもすぐに応用できます。@functools.wraps(func)・*args・**kwargsの3点セットがデコレータの基本形です。

引数付きデコレータの書き方

続いては、デコレータ自体に引数を渡せるようにする方法を確認していきます。

デコレータに引数を渡したい場合は、デコレータを返す関数(デコレータファクトリ)をもう1つ外側に用意するパターンで実現できます。

引数付きデコレータの基本パターン


import functools

# 引数付きデコレータ(デコレータファクトリ)
def repeat(times):
    """指定した回数だけ関数を繰り返すデコレータ"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def say_item(name):
    print(f"商品:{name}")

say_item("アボカド")

# 出力結果:商品:アボカド
# 出力結果:商品:アボカド
# 出力結果:商品:アボカド

@repeat(3)はrepeat(3)を呼び出してdecoratorを取得し、そのdecoratorをsay_itemに適用するという2段階の処理になっています。引数付きデコレータは3層のネストした関数で実現します。

リトライ機能を持つ引数付きデコレータ


import functools
import random
import time

def retry(max_retries=3, delay=0.1):
    """エラー時に指定回数リトライするデコレータ"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_retries + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_retries:
                        print(f"最大リトライ数に達しました:{e}")
                        raise
                    print(f"試行{attempt}回目失敗:{e}。リトライします...")
                    time.sleep(delay)
        return wrapper
    return decorator

# 50%の確率で失敗する関数をシミュレート
@retry(max_retries=4, delay=0.05)
def fetch_data(item_name):
    if random.random() < 0.5:
        raise ConnectionError(f"{item_name}のデータ取得に失敗")
    return f"{item_name}のデータ取得成功"

result = fetch_data("サーモン")
print(result)

# 出力結果:試行1回目失敗:サーモンのデータ取得に失敗。リトライします...(毎回変わります)
# 出力結果:サーモンのデータ取得成功

最大リトライ回数と待機時間を引数で指定できるretryデコレータはAPI通信・ファイル操作など一時的なエラーが起きやすい処理に実務でよく使われるパターンです。

実践的なデコレータのパターン

続いては、実務でよく使われる実践的なデコレータのパターンを確認していきます。

デコレータは実行時間の計測・キャッシュ・ログ記録・入力値のバリデーションなど横断的な処理を関数から切り離して再利用するのに最適な仕組みです。

実行時間を計測するデコレータ


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 process_items(items):
    """アイテムを処理する"""
    total = sum(price for _, price in items)
    time.sleep(0.1)  # 処理をシミュレート
    return total

items = [("アボカド", 298), ("サーモン", 1200), ("キーボード", 8500)]
total = process_items(items)
print(f"合計:{total:,}円")

# 出力結果:「process_items」の実行時間:0.1012秒
# 出力結果:合計:9,998円

timerデコレータを付けるだけで任意の関数の実行時間を計測できます。パフォーマンス最適化の際にボトルネックを特定するのに役立つパターンです。

キャッシュデコレータ(メモ化)


import functools

# functools.lru_cache:標準ライブラリのキャッシュデコレータ
@functools.lru_cache(maxsize=128)
def fibonacci(n):
    """フィボナッチ数をキャッシュ付きで計算"""
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

import time

# キャッシュなしでは再帰で同じ計算を何度も行うが
# lru_cacheで計算結果をキャッシュして高速化
start = time.perf_counter()
result = fibonacci(35)
elapsed = time.perf_counter() - start

print(f"fibonacci(35) = {result}")
print(f"実行時間:{elapsed:.4f}秒")
print(f"キャッシュ情報:{fibonacci.cache_info()}")

# 出力結果:fibonacci(35) = 9227465
# 出力結果:実行時間:0.0001秒
# 出力結果:キャッシュ情報:CacheInfo(hits=33, misses=36, maxsize=128, currsize=36)

functools.lru_cache()は標準ライブラリに用意されたキャッシュデコレータです。同じ引数での呼び出し結果を自動的にキャッシュし、再計算を省いて処理を高速化します。

複数のデコレータを重ねて適用する


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"実行時間:{elapsed:.4f}秒")
        return result
    return wrapper

def logger(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"開始:{func.__name__}({args}, {kwargs})")
        result = func(*args, **kwargs)
        print(f"終了:戻り値={result}")
        return result
    return wrapper

# 複数のデコレータを重ねて適用(下から上に適用される)
@timer
@logger
def calculate(price, quantity):
    """価格×数量を計算する"""
    return price * quantity

result = calculate(298, 50)
print(f"結果:{result}円")

# 出力結果:開始:calculate((298, 50), {})
# 出力結果:終了:戻り値=14900
# 出力結果:実行時間:0.0001秒
# 出力結果:結果:14900円

複数のデコレータは下から上の順で適用されます。@timerが外側・@loggerが内側に適用されるため、timerがlogger全体の実行時間を計測します。それぞれのデコレータが独立した機能を担うことでコードの再利用性が高まります。

まとめ

この記事では、Pythonのデコレータの仕組みと使い方について、基本的な@記法・引数付きデコレータ・functools.wraps・実践的なパターンまで幅広く解説しました。

デコレータは関数を受け取り機能を追加した新しい関数を返す仕組みです。*argsと**kwargsで汎用的なwrapper関数を書き、functools.wrapsで元の関数の情報を保持するのが基本テンプレートです。引数付きデコレータは3層のネストで実現できます。実行時間計測・リトライ処理・キャッシュ(lru_cache)など横断的な処理をデコレータにまとめることでコードの重複を大幅に削減できます。

今回紹介したデコレータのパターンは実務のAPIサーバー・バッチ処理・CLIツールなど様々な場面で活用できます。ぜひ実際のプロジェクトで試してみてください。

デコレータを書くときは必ずfunctools.wrapsを付けましょう。wrapsなしでは関数名やdocstringが失われ、デバッグやhelp()での情報確認に支障が出ます。また引数と戻り値を持つ関数への適用を想定してwrapper関数は常に*argsと**kwargsを受け取りreturn result を返す形にしておくことで、汎用的で安全なデコレータが実現できます。