はじめに
デコレータは関数やクラスの振る舞いを変更する仕組みです。ログ出力、認証チェック、キャッシュなど、共通処理を追加するのに便利です。
基本概念
関数は第一級オブジェクト
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
まとめ
- デコレータは関数の振る舞いを変更する
@decoratorはfunc = decorator(func)の糖衣構文@functools.wrapsでメタデータを保持- 引数付きデコレータは3重のネスト
- クラスもデコレータになれる
- 複数デコレータは下から上に適用される
これでPython応用構文シリーズは完了です。