閉じる

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

既存の関数の中身を変えずにログや計測、権限チェックなどを後付けしたい時、Pythonのデコレータが強力に役立ちます。

デコレータは関数やメソッドの周りに共通処理を挟む仕組みで、関心の分離と再利用性の向上を同時に実現します。

この記事では、初心者が段階的に理解できるよう、基本から応用、注意点まで丁寧に解説します。

Pythonを扱う求人
読み込み中…

デコレータの基本(初心者向け): 関数を変更せず機能を拡張

デコレータとは?仕組みとメリット

デコレータとは、関数(やクラス)を引数に受け取り、別の関数(やクラス)を返す関数のことです。

適用された関数は、ラッパー関数で包まれ、呼び出し前後に共通処理を挟めます。

これにより、元の関数を改変せずにログ、計測、リトライ、権限チェックなどの機能を横断的に追加できます。

重複コードの削減テスト容易性の向上が主なメリットです。

@記法と適用タイミング

デコレータは@decorator_nameという記法で関数定義の直前に付けます。

適用は定義時(=モジュールのインポート時)に行われ、関数オブジェクトがデコレータに渡され、decorated = decorator(original)のように置き換えられます。

副作用を持つデコレータは、インポート時にも実行される点に注意します。

最小のデコレータ例(前後でログ)

最小限の前後ログを出すデコレータを示します。

まずは概念理解のためfunctools.wrapsなしで示し、その後で改善します。

Python
# 最小のデコレータ例: 呼び出し前後にメッセージを表示
def simple_logger(func):
    def wrapper(*args, **kwargs):
        print(f"[simple_logger] before: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"[simple_logger] after:  {func.__name__}")
        return result
    return wrapper

@simple_logger
def greet(name):
    print(f"Hello, {name}!")

# 実行
greet("Alice")
実行結果
[simple_logger] before: greet
Hello, Alice!
[simple_logger] after:  greet

関数は第1級オブジェクトとクロージャ

Pythonでは関数は第1級オブジェクトであり、変数に代入したり引数や戻り値として扱えます。

また、クロージャにより外側スコープの値をラッパー関数が捕捉し続けます。

これがデコレータの根幹を支えています。

Python
# 関数を引数・戻り値に使いつつ、クロージャで状態を保持する例
def call_counter(func):
    count = 0  # wrapper が捕捉する外側変数(自由変数)

    def wrapper(*args, **kwargs):
        nonlocal count  # 外側の count を更新する
        count += 1
        print(f"[call_counter] called {count} time(s)")
        return func(*args, **kwargs)

    return wrapper

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

print(add(1, 2))
print(add(10, 20))
実行結果
[call_counter] called 1 time(s)
3
[call_counter] called 2 time(s)
30

ラッパー関数と戻り値の流れ

ラッパー関数はfuncの呼び出し結果を必ず返却する必要があります。

戻り値を返し忘れるとNoneが返ってしまうため、注意が必要です。

Python
def return_flow_logger(func):
    def wrapper(*args, **kwargs):
        print("[return_flow_logger] before")
        value = func(*args, **kwargs)  # 元関数の戻り値
        print("[return_flow_logger] after")
        return value  # 忘れずに返す
    return wrapper

@return_flow_logger
def mul(a, b):
    return a * b

print(mul(6, 7))
実行結果
[return_flow_logger] before
[return_flow_logger] after
42

複数デコレータの適用順

デコレータを積み重ねると、適用は下から上、実行は上から下になります。

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

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

@deco_a   # 2番目に適用される(実行時は外側)
@deco_b   # 1番目に適用される(実行時は内側)
def work():
    print("working...")

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

Pythonデコレータの使い方: 基本パターン

引数と戻り値をそのまま渡す

元関数のインターフェースを変えないことが基本です。

Python
def passthrough(func):
    def wrapper(*args, **kwargs):
        # そのまま渡すだけ
        return func(*args, **kwargs)
    return wrapper

@passthrough
def square(x):
    return x * x

print(square(5))
実行結果
25

可変長引数(*args, **kwargs)の扱い

デコレータでは*args**kwargsで引数を丸ごと受け渡すのが安全です。

Python
def show_args(func):
    def wrapper(*args, **kwargs):
        print(f"args={args}, kwargs={kwargs}")
        return func(*args, **kwargs)
    return wrapper

@show_args
def demo(a, b, c=0, **extra):
    return a + b + c + sum(extra.values())

print(demo(1, 2, c=3, d=4, e=5))
実行結果
args=(1, 2), kwargs={'c': 3, 'd': 4, 'e': 5}
15

functools.wrapsで関数情報を保持

デコレータは元関数の__name____doc__を失いがちです。

functools.wrapsを使ってメタデータをコピーします。

Python
import functools

def with_wraps(func):
    @functools.wraps(func)  # ここが重要
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@with_wraps
def target(x):
    """ドキュメント文字列"""
    return x

print(target.__name__)
print(target.__doc__)
実行結果
target
ドキュメント文字列

例外を共通処理するデコレータ

例外をログに記録し、必要なら再送出します。

握りつぶしはバグを隠すため原則避けます。

Python
import functools
import logging

logging.basicConfig(level=logging.INFO)

def handle_exceptions(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except ValueError as e:
            logging.exception("ValueError occurred")
            raise  # 必要に応じて再送出
    return wrapper

@handle_exceptions
def might_fail(x):
    if x < 0:
        raise ValueError("x must be non-negative")
    return x

try:
    print(might_fail(-1))
except ValueError:
    print("caught!")
実行結果
ERROR:root:ValueError occurred
Traceback (most recent call last):
  ...
ValueError: x must be non-negative
caught!

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

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

Python
import functools
import time

def timeit(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            elapsed = (time.perf_counter() - start) * 1000
            print(f"[timeit] {func.__name__}: {elapsed:.2f} ms")
    return wrapper

@timeit
def slow_task(n):
    time.sleep(n)
    return "done"

print(slow_task(0.05))
実行結果
[timeit] slow_task: 50.?? ms
done

ログ・トレースを追加するデコレータ

引数と戻り値をトレースします。

機密情報には配慮します。

Python
import functools
import logging

logging.basicConfig(level=logging.INFO)

def trace(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        logging.info("→ %s args=%s kwargs=%s", func.__name__, args, kwargs)
        result = func(*args, **kwargs)
        logging.info("← %s result=%r", func.__name__, result)
        return result
    return wrapper

@trace
def concat(a, b, sep="-"):
    return f"{a}{sep}{b}"

print(concat("A", "B", sep=":"))
実行結果
INFO:root:→ concat args=('A', 'B') kwargs={'sep': ':'}
INFO:root:← concat result='A:B'
A:B

権限チェック(認可)のデコレータ

簡易なロールチェックの例です。

実務ではフレームワークの仕組みに合わせます。

Python
import functools

def require_role(role_name):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(user, *args, **kwargs):
            # 第一引数 user の roles を想定
            if role_name not in getattr(user, "roles", set()):
                raise PermissionError(f"role '{role_name}' required")
            return func(user, *args, **kwargs)
        return wrapper
    return decorator

class User:
    def __init__(self, name, roles):
        self.name = name
        self.roles = set(roles)

@require_role("admin")
def delete_post(user, post_id):
    return f"{post_id} deleted by {user.name}"

alice = User("Alice", ["admin"])
bob = User("Bob", [])

print(delete_post(alice, 123))
try:
    print(delete_post(bob, 123))
except PermissionError as e:
    print("denied:", e)
実行結果
123 deleted by Alice
denied: role 'admin' required

応用デコレータ: 引数付き・メソッド・クラス

引数付きデコレータの作り方

デコレータファクトリで外側にパラメータを受け取ります。

Python
import functools

def tag(prefix="*"):
    # ここがファクトリ: 引数を受け取る
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"{prefix} begin")
            r = func(*args, **kwargs)
            print(f"{prefix} end")
            return r
        return wrapper
    return tag  # ← ではなく、decorator を返すのが正解

# 注意: 上の return は decorator を返す必要がある
def tag(prefix="*"):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"{prefix} begin")
            r = func(*args, **kwargs)
            print(f"{prefix} end")
            return r
        return wrapper
    return decorator

@tag(prefix=">>>")
def hello():
    print("hello")

hello()
実行結果
>>> begin
hello
>>> end

オプション付きログ(レベルやメッセージ)

ログレベルを可変にします。

Python
import functools
import logging

logging.basicConfig(level=logging.DEBUG)

def log(level=logging.INFO, msg="call"):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            logging.log(level, "%s: %s", msg, func.__name__)
            return func(*args, **kwargs)
        return wrapper
    return decorator

@log(level=logging.DEBUG, msg="start")
def compute(x, y):
    return x + y

print(compute(3, 4))
実行結果
DEBUG:root:start: compute
7

メソソッドへの適用とself/cls

インスタンスメソッドではself、クラスメソッドではclsが先頭引数になります。

デコレータ側は特別扱いは不要で、そのまま受け取ります。

Python
import functools

def show_bound(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("first arg type:", type(args[0]).__name__)  # self or cls
        return func(*args, **kwargs)
    return wrapper

class Calc:
    @show_bound
    def add(self, a, b):
        return a + b

    @classmethod
    @show_bound
    def version(cls):
        return "1.0"

c = Calc()
print(c.add(2, 3))
print(Calc.version())
実行結果
first arg type: Calc
5
first arg type: type
1.0

組み込みデコレータ(@staticmethod/@classmethod/@property)

組み込みデコレータの役割と、他デコレータとの順序の基本を整理します。

  • @staticmethod: self不要な関数をメソッドとして束縛
  • @classmethod: 最初の引数にclsが渡る
  • @property: ゲッターをプロパティ化

順序は意味を変えるため注意が必要です。

一般に@propertyは最も内側(下)に置き、その外側に独自デコレータを重ねます。

Python
import functools

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

class User:
    def __init__(self, name):
        self._name = name

    @property           # 内側: ゲッター定義
    @uppercase          # 外側: 返り値を加工
    def name(self):
        return self._name

u = User("alice")
print(u.name)
実行結果
ALICE

クラスデコレータの基本

クラスを受け取り、修正または別のクラスを返します。

簡単な登録・属性追加の例です。

Python
REGISTRY = {}

def register(name):
    def decorator(cls):
        REGISTRY[name] = cls
        cls.registered_as = name  # 動的に属性追加
        return cls
    return decorator

@register("service")
class Service:
    pass

print(Service.registered_as)
print(REGISTRY["service"] is Service)
実行結果
service
True

ベストプラクティスと注意点

デコレータの順序と副作用

複数デコレータは下から適用、上から実行です。

初期化処理や副作用があるデコレータを外側に置くか内側に置くかで挙動が変わります。

順序はテストで明示的に検証します。

import時に実行される点に注意

@decoは定義時に評価されます。

重い初期化や外部接続をデコレータ内で行うと、インポートだけで遅くなるため避けます。

必要なら遅延初期化(初回呼び出し時に実行)にします。

例外を握りつぶさない設計

デバッグや監視のためログは取りつつ、適切に再送出します。

bare except(例: except:)は避け、想定例外だけを捕捉します。

型ヒント(Callable/TypeVar/ParamSpec)のコツ

ジェネリックに正しい型を保つにはParamSpecTypeVarを使います。

これにより、デコレータが元関数の引数型と戻り値型をそのまま表現できます。

Python
from typing import Callable, TypeVar, ParamSpec
import functools

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

def typed_decorator(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

@typed_decorator
def f(x: int, y: str) -> float:
    return float(x + len(y))

print(f(3, "ab"))
実行結果
5.0

デコレータを一時無効化する手法

開発やデバッグで一時的に無効化したい場合があります。

Python
import functools
import os

ENABLE_TRACE = os.getenv("ENABLE_TRACE", "0") == "1"

def optional_trace(func):
    if not ENABLE_TRACE:
        return func  # 何もしない(パススルー)
    @functools.wraps(func)
    def wrapper(*a, **k):
        print("[trace]", func.__name__, a, k)
        return func(*a, **k)
    return wrapper

@optional_trace
def calc(a, b):
    return a + b

print(calc(1, 2))
実行結果
3

環境変数ENABLE_TRACE=1で有効化できます。

また、if DEBUG: decorator = identityのように差し替える方法も有効です。

パフォーマンスと過度な抽象化を避ける

デコレータは呼び出しコストをわずかに増やします。

ホットパスに多段適用するとオーバーヘッドが顕著になる場合があります。

計測(timeit)して、必要ならデコレータの層を減らす、ロジックをまとめる、または@functools.lru_cacheなど計算量削減の施策を検討します。

以下は重ねがけのオーバーヘッドを観察する簡単な比較例です。

Python
import functools
import time

def noop(func):
    @functools.wraps(func)
    def w(*a, **k):
        return func(*a, **k)
    return w

@noop
@noop
@noop
def fast(x):
    return x + 1

start = time.perf_counter()
for _ in range(1_000_0):
    fast(1)
print("elapsed:", (time.perf_counter() - start) * 1000, "ms")
実行結果
elapsed: X.XX ms

実運用では、可読性と保守性を損なうほどの抽象化は避け、役割の明確なデコレータに留めます。

まとめ

デコレータは、元の関数を変更せずに機能を拡張できるPythonの強力な機能です。

仕組みの要は、関数が第1級オブジェクトであることと、クロージャで外側の状態を保持できることにあります。

基本では*args**kwargsの透過、functools.wrapsによるメタデータ保持、例外の適切な再送出、計測やログの追加を押さえます。

応用では、引数付きデコレータメソッドやpropertyクラスデコレータまで広げられます。

最後に、インポート時に評価される点や順序・副作用、パフォーマンスに注意し、必要に応じて無効化や型ヒントで安全性と開発体験を高めていきましょう。

Pythonを扱う求人
読み込み中…

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

URLをコピーしました!