11
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

@ から始まるアレ、なんとなく使ってるけど仕組みがわからない。

この記事では、デコレータの正体を図解で完全理解するよ。

デコレータの正体

@ は糖衣構文

@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

これでデコレータ完全理解!🎄

11
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?