はじめに
@ から始まるアレ、なんとなく使ってるけど仕組みがわからない。
この記事では、デコレータの正体を図解で完全理解するよ。
デコレータの正体
@ は糖衣構文
@decorator
def func():
pass
これは以下と完全に同じ:
def func():
pass
func = decorator(func)
つまり デコレータは関数を受け取って関数を返す関数。
図解
┌─────────────────────────────────────────────┐
│ @decorator │
│ def func(): │
│ pass │
└─────────────────────────────────────────────┘
↓ 変換
┌─────────────────────────────────────────────┐
│ func = decorator(func) │
│ │
│ decorator: func → wrapper → 新しいfunc │
└─────────────────────────────────────────────┘
最もシンプルなデコレータ
def make_uppercase(func):
def wrapper():
return func().upper()
return wrapper
@make_uppercase
def greet():
return "hello"
print(greet()) # => HELLO
図解
greet() が呼ばれると:
1. wrapper() が実行される
2. wrapper内で 元のgreet() を呼ぶ
3. 結果 "hello" を .upper() で変換
4. "HELLO" を返す
greet()
│
▼
┌─────────┐
│ wrapper │ ← 実際はこれが呼ばれる
└────┬────┘
│ func() を呼ぶ
▼
┌─────────┐
│ 元greet │
└────┬────┘
│ "hello"
▼
.upper() で変換
│
▼
"HELLO"
引数を取る関数のデコレータ
*args, **kwargs で何でも受け取る:
import functools
def debug(func):
@functools.wraps(func) # 元の関数名を保持
def wrapper(*args, **kwargs):
print(f"[DEBUG] {func.__name__} called")
print(f"[DEBUG] args: {args}, kwargs: {kwargs}")
result = func(*args, **kwargs)
print(f"[DEBUG] returned: {result}")
return result
return wrapper
@debug
def add(a, b):
return a + b
add(2, 3)
# [DEBUG] add called
# [DEBUG] args: (2, 3), kwargs: {}
# [DEBUG] returned: 5
functools.wraps が重要
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@bad_decorator
def my_func():
"""My docstring"""
pass
print(my_func.__name__) # => wrapper ← 元の名前が消えた!
print(my_func.__doc__) # => None ← docstringも消えた!
@functools.wraps(func) をつけると:
def good_decorator(func):
@functools.wraps(func) # これを追加
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@good_decorator
def my_func():
"""My docstring"""
pass
print(my_func.__name__) # => my_func ← 保持されてる!
print(my_func.__doc__) # => My docstring
引数を取るデコレータ
デコレータ自体に引数を渡したい場合は3層構造:
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(times=3)
def say_hello(name):
print(f"Hello, {name}!")
say_hello("World")
# Hello, World!
# Hello, World!
# Hello, World!
図解
@repeat(times=3) ← repeat(times=3) が decorator を返す
def say_hello(): ← decorator(say_hello) が wrapper を返す
...
┌────────────────┐
│ repeat(times=3)│
└───────┬────────┘
│ returns
▼
┌────────────────┐
│ decorator │
└───────┬────────┘
│ decorator(say_hello)
▼
┌────────────────┐
│ wrapper │ ← say_hello はこれになる
└────────────────┘
実用デコレータ集
実行時間計測
def timer(func):
@functools.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 "done"
slow_function() # slow_function: 1.0012秒
呼び出し回数カウント(クラスベース)
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
return self.func(*args, **kwargs)
@CountCalls
def process():
return "done"
process()
process()
print(process.count) # => 2
キャッシュ(メモ化)
def memoize(func):
cache = {}
@functools.wraps(func)
def wrapper(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
fibonacci(100) # 一瞬で計算(キャッシュのおかげ)
💡 Python 3.9+なら
@functools.cacheが使える
リトライ
def retry(max_attempts=3, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
print(f"Attempt {attempt} failed: {e}")
if attempt == max_attempts:
raise
return wrapper
return decorator
@retry(max_attempts=3, exceptions=(ConnectionError,))
def fetch_data():
# ネットワークエラーが起きても3回まで再試行
...
複数デコレータの重ね方
@debug
@timer
def complex_operation(n):
time.sleep(0.1)
return n * n
これは以下と同じ:
complex_operation = debug(timer(complex_operation))
実行順序
complex_operation(5) 呼び出し
│
▼
┌────────────────┐
│ debug の │ ← 1. 最初に実行
│ wrapper │
└───────┬────────┘
│
▼
┌────────────────┐
│ timer の │ ← 2. 次に実行
│ wrapper │
└───────┬────────┘
│
▼
┌────────────────┐
│ 元の関数 │ ← 3. 最後に実行
│ complex_op │
└────────────────┘
標準ライブラリのデコレータ
@staticmethod / @classmethod
class MyClass:
@staticmethod
def static_method():
# selfなし
pass
@classmethod
def class_method(cls):
# clsを受け取る
pass
@property
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
return 3.14 * self._radius ** 2
c = Circle(5)
print(c.area) # メソッド呼び出しではなくプロパティアクセス
@functools.cache(Python 3.9+)
@functools.cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
@functools.lru_cache
@functools.lru_cache(maxsize=128)
def expensive_calculation(n):
...
@dataclasses.dataclass
from dataclasses import dataclass
@dataclass
class User:
id: int
name: str
まとめ
| 概念 | 説明 |
|---|---|
| デコレータ | 関数を受け取って関数を返す関数 |
@decorator |
func = decorator(func) の糖衣構文 |
@functools.wraps |
元の関数のメタデータを保持 |
*args, **kwargs |
任意の引数を受け取る |
| 引数付きデコレータ | 3層構造(関数→デコレータ→ラッパー) |
デコレータを作る時のテンプレート
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 前処理
result = func(*args, **kwargs)
# 後処理
return result
return wrapper
これでデコレータ完全理解!🎄