チュートリアル

Python応用構文:デコレータ

Pythonデコレータ応用
広告エリア

はじめに

デコレータは関数やクラスの振る舞いを変更する仕組みです。ログ出力、認証チェック、キャッシュなど、共通処理を追加するのに便利です。

基本概念

関数は第一級オブジェクト

def greet(name):
    return f"こんにちは、{name}!"

# 関数を変数に代入
hello = greet
print(hello("太郎"))  # こんにちは、太郎!

# 関数を引数として渡す
def call_twice(func, arg):
    print(func(arg))
    print(func(arg))

call_twice(greet, "花子")

# 関数内で関数を定義
def outer():
    def inner():
        return "内側の関数"
    return inner

func = outer()
print(func())  # 内側の関数

最初のデコレータ

def my_decorator(func):
    def wrapper():
        print("関数実行前")
        func()
        print("関数実行後")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()
# 出力:
# 関数実行前
# Hello!
# 関数実行後

# @構文なしで書くと
def say_hello():
    print("Hello!")
say_hello = my_decorator(say_hello)

引数を受け取るデコレータ

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"引数: {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"戻り値: {result}")
        return result
    return wrapper

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

result = add(3, 5)
# 出力:
# 引数: (3, 5), {}
# 戻り値: 8

functools.wrapsを使う

デコレートされた関数のメタデータを保持します。

from functools import wraps

def my_decorator(func):
    @wraps(func)  # これが重要
    def wrapper(*args, **kwargs):
        """ラッパー関数"""
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def greet(name):
    """挨拶する関数"""
    return f"Hello, {name}!"

print(greet.__name__)  # greet(wrapsなしだとwrapper)
print(greet.__doc__)   # 挨拶する関数(wrapsなしだとラッパー関数)

実践的なデコレータ

実行時間計測

import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__}: {end - start:.4f}秒")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "完了"

slow_function()  # slow_function: 1.0012秒

ログ出力

import logging
from functools import wraps

logging.basicConfig(level=logging.INFO)

def log_calls(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        logging.info(f"呼び出し: {func.__name__}({args}, {kwargs})")
        try:
            result = func(*args, **kwargs)
            logging.info(f"成功: {func.__name__} -> {result}")
            return result
        except Exception as e:
            logging.error(f"エラー: {func.__name__} -> {e}")
            raise
    return wrapper

@log_calls
def divide(a, b):
    return a / b

divide(10, 2)  # INFO: 呼び出し: divide((10, 2), {})
               # INFO: 成功: divide -> 5.0

リトライ

import time
from functools import wraps

def retry(max_attempts=3, delay=1):
    def decorator(func):
        @wraps(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}. {delay}秒後にリトライ...")
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(max_attempts=3, delay=2)
def unreliable_api():
    import random
    if random.random() < 0.7:
        raise ConnectionError("接続エラー")
    return "成功"

キャッシュ

from functools import wraps

def cache(func):
    """シンプルなメモ化"""
    memo = {}

    @wraps(func)
    def wrapper(*args):
        if args not in memo:
            memo[args] = func(*args)
        return memo[args]
    return wrapper

@cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(100))  # 高速に計算

# 標準ライブラリを使う場合
from functools import lru_cache

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

認証チェック

from functools import wraps

def require_auth(func):
    @wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.get("is_authenticated"):
            raise PermissionError("認証が必要です")
        return func(user, *args, **kwargs)
    return wrapper

def require_role(role):
    def decorator(func):
        @wraps(func)
        def wrapper(user, *args, **kwargs):
            if role not in user.get("roles", []):
                raise PermissionError(f"{role}ロールが必要です")
            return func(user, *args, **kwargs)
        return wrapper
    return decorator

@require_auth
@require_role("admin")
def delete_user(user, user_id):
    print(f"ユーザー {user_id} を削除しました")

admin = {"is_authenticated": True, "roles": ["admin"]}
delete_user(admin, 123)

引数付きデコレータ

from functools import wraps

def repeat(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def say_hello():
    print("Hello!")

say_hello()
# Hello!
# Hello!
# Hello!

クラスデコレータ

関数をデコレートするクラス

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"{self.func.__name__}{self.count}回呼び出されました")
        return self.func(*args, **kwargs)

@CountCalls
def greet(name):
    return f"Hello, {name}!"

greet("太郎")  # greetは1回呼び出されました
greet("花子")  # greetは2回呼び出されました

クラスをデコレート

def add_method(cls):
    def new_method(self):
        return f"追加されたメソッド: {self}"
    cls.new_method = new_method
    return cls

@add_method
class MyClass:
    pass

obj = MyClass()
print(obj.new_method())

# シングルトンパターン
def singleton(cls):
    instances = {}
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class Database:
    def __init__(self):
        print("データベース接続")

db1 = Database()  # データベース接続
db2 = Database()  # 何も出力されない
print(db1 is db2)  # True

デコレータの組み合わせ

@decorator1
@decorator2
@decorator3
def func():
    pass

# 以下と同じ
func = decorator1(decorator2(decorator3(func)))
# 適用順序: decorator3 -> decorator2 -> decorator1
# 実行順序: decorator1 -> decorator2 -> decorator3 -> func

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

# @property - ゲッター/セッター
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        self._radius = max(0, value)

# @staticmethod, @classmethod
class MyClass:
    @staticmethod
    def static_method():
        return "静的メソッド"

    @classmethod
    def class_method(cls):
        return f"クラスメソッド: {cls.__name__}"

# @dataclass
from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int

まとめ

  • デコレータは関数の振る舞いを変更する
  • @decoratorfunc = decorator(func)の糖衣構文
  • @functools.wrapsでメタデータを保持
  • 引数付きデコレータは3重のネスト
  • クラスもデコレータになれる
  • 複数デコレータは下から上に適用される

これでPython応用構文シリーズは完了です。

広告エリア