Easy📖Теория6 min

Декораторы

Функции-декораторы, декораторы с аргументами, @functools.wraps и практические примеры

Декораторы

Декораторы -- один из самых элегантных паттернов Python. Они позволяют модифицировать поведение функций и классов без изменения их исходного кода.

Что такое декоратор

Декоратор -- это функция, которая принимает функцию и возвращает новую (модифицированную) функцию:

# Decorator is just syntactic sugar
def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("До вызова")
        result = func(*args, **kwargs)
        print("После вызова")
        return result
    return wrapper

# These two are equivalent:
@my_decorator
def greet(name: str) -> str:
    return f"Привет, {name}!"

# Same as: greet = my_decorator(greet)

print(greet("Иван"))
# До вызова
# После вызова
# Привет, Иван!

@functools.wraps -- обязательный элемент

Без @wraps декоратор теряет метаданные исходной функции:

import functools

def bad_decorator(func):
    """Decorator WITHOUT @wraps."""
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@bad_decorator
def hello():
    """Say hello."""
    pass

print(hello.__name__)  # 'wrapper' (WRONG! Should be 'hello')
print(hello.__doc__)   # None (WRONG! Should be 'Say hello.')

# CORRECT: always use @functools.wraps
def good_decorator(func):
    """Decorator WITH @wraps."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@good_decorator
def hello():
    """Say hello."""
    pass

print(hello.__name__)  # 'hello'
print(hello.__doc__)   # 'Say hello.'
print(hello.__wrapped__)  # original function (access through __wrapped__)

Практические декораторы

Замер времени выполнения

import functools
import time

def timer(func):
    """Measure and print execution time."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__}: {elapsed:.4f}с")
        return result
    return wrapper

@timer
def slow_function():
    """Simulate slow work."""
    time.sleep(0.5)
    return "done"

slow_function()  # slow_function: 0.5003с

Логирование вызовов

import functools
import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

def log_calls(func):
    """Log function calls with arguments and return value."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        logger.debug(f"Вызов {func.__name__}({signature})")
        try:
            result = func(*args, **kwargs)
            logger.debug(f"{func.__name__} вернул {result!r}")
            return result
        except Exception as e:
            logger.exception(f"{func.__name__} вызвал исключение: {e}")
            raise
    return wrapper

@log_calls
def add(a: int, b: int) -> int:
    return a + b

add(2, 3)
# DEBUG: Вызов add(2, 3)
# DEBUG: add вернул 5

Retry с экспоненциальной задержкой

import functools
import time
from typing import TypeVar, Callable, Any

def retry(max_attempts: int = 3, delay: float = 1.0, backoff: float = 2.0):
    """Retry decorator with exponential backoff."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            current_delay = delay
            last_exception = None
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    if attempt < max_attempts:
                        print(f"Попытка {attempt} неудачна: {e}. "
                              f"Повтор через {current_delay:.1f}с...")
                        time.sleep(current_delay)
                        current_delay *= backoff
            raise last_exception
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5, backoff=2.0)
def unreliable_api_call() -> str:
    """Simulate an unreliable API."""
    import random
    if random.random() < 0.7:
        raise ConnectionError("Сервер недоступен")
    return "OK"

Кэширование (Memoization)

import functools

def cache(func):
    """Simple memoization decorator."""
    memo = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args not in memo:
            memo[args] = func(*args)
        return memo[args]
    wrapper.cache = memo  # expose cache for debugging
    wrapper.cache_clear = memo.clear
    return wrapper

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

print(fibonacci(100))  # 354224848179261915075

# Better: use built-in functools.lru_cache
@functools.lru_cache(maxsize=128)
def factorial(n: int) -> int:
    return 1 if n <= 1 else n * factorial(n - 1)

print(factorial(20))  # 2432902008176640000
print(factorial.cache_info())
# CacheInfo(hits=0, misses=21, maxsize=128, currsize=21)

# Python 3.9+: functools.cache (unlimited cache)
@functools.cache
def expensive_computation(x: int) -> int:
    """Unlimited cache (no maxsize)."""
    print(f"Computing for {x}...")
    return x ** 3

print(expensive_computation(5))  # Computing for 5... 125
print(expensive_computation(5))  # 125 (cached, no print)

Валидация аргументов

import functools

def validate_types(**type_hints):
    """Validate function argument types at runtime."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # Get function parameter names
            import inspect
            sig = inspect.signature(func)
            bound = sig.bind(*args, **kwargs)
            bound.apply_defaults()

            for param_name, expected_type in type_hints.items():
                if param_name in bound.arguments:
                    value = bound.arguments[param_name]
                    if not isinstance(value, expected_type):
                        raise TypeError(
                            f"Аргумент '{param_name}' должен быть {expected_type.__name__}, "
                            f"получен {type(value).__name__}"
                        )
            return func(*args, **kwargs)
        return wrapper
    return decorator

@validate_types(name=str, age=int)
def create_user(name: str, age: int) -> dict:
    return {"name": name, "age": age}

print(create_user("Иван", 25))       # {'name': 'Иван', 'age': 25}
# create_user("Иван", "25")          # TypeError: Аргумент 'age' должен быть int

Стекирование декораторов

import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        print(f"Время: {time.perf_counter() - start:.4f}с")
        return result
    return wrapper

def log_result(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print(f"Результат: {result}")
        return result
    return wrapper

# Decorators apply from bottom to top
@timer          # 2nd: wraps the result of @log_result
@log_result     # 1st: wraps the original function
def compute(n: int) -> int:
    return sum(range(n))

compute(1_000_000)
# Результат: 499999500000
# Время: 0.0312с

# Equivalent to: compute = timer(log_result(compute))

Декораторы-классы

import functools

class CountCalls:
    """Decorator that counts function calls."""

    def __init__(self, func):
        functools.update_wrapper(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)

    def reset(self):
        self.count = 0

@CountCalls
def greet(name: str) -> str:
    return f"Привет, {name}!"

greet("Иван")   # greet вызвана 1 раз
greet("Мария")  # greet вызвана 2 раз
print(f"Всего вызовов: {greet.count}")  # 2
greet.reset()

# Class decorator with arguments
class RateLimit:
    """Limit function calls per time window."""

    def __init__(self, max_calls: int, period: float):
        self.max_calls = max_calls
        self.period = period
        self.calls = []

    def __call__(self, func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            import time
            now = time.time()
            # Remove old calls outside the window
            self.calls = [t for t in self.calls if now - t < self.period]
            if len(self.calls) >= self.max_calls:
                raise RuntimeError(f"Rate limit: max {self.max_calls} calls per {self.period}s")
            self.calls.append(now)
            return func(*args, **kwargs)
        return wrapper

@RateLimit(max_calls=3, period=10.0)
def api_call(endpoint: str) -> str:
    return f"Response from {endpoint}"

Встроенные декораторы

# @staticmethod - no access to instance or class
# @classmethod - receives class as first argument
# @property - getter/setter as attribute access
# @functools.lru_cache - memoization
# @functools.cache - unlimited memoization (Python 3.9+)
# @functools.singledispatch - function overloading by type
# @dataclasses.dataclass - auto-generate __init__, __repr__, etc.
# @abc.abstractmethod - abstract methods in ABC

# @functools.singledispatch example
from functools import singledispatch

@singledispatch
def format_value(value) -> str:
    """Format a value to string."""
    return str(value)

@format_value.register(int)
def _(value: int) -> str:
    return f"{value:,}"

@format_value.register(float)
def _(value: float) -> str:
    return f"{value:.2f}"

@format_value.register(list)
def _(value: list) -> str:
    return f"[{', '.join(str(x) for x in value)}]"

print(format_value(1000000))    # 1,000,000
print(format_value(3.14159))    # 3.14
print(format_value([1, 2, 3]))  # [1, 2, 3]
print(format_value("hello"))    # hello

Итоги

  • Декоратор -- функция, принимающая функцию и возвращающая новую функцию
  • @functools.wraps -- обязательно для сохранения метаданных
  • Декоратор с аргументами -- три уровня вложенности
  • Стекирование: @A @B def f() = f = A(B(f)) (снизу вверх)
  • @functools.lru_cache / @functools.cache -- встроенное кэширование
  • @functools.singledispatch -- перегрузка по типу первого аргумента
  • Классы-декораторы -- когда нужно хранить состояние
  • Практические применения: timing, logging, retry, validation, rate limiting

Проверь себя

🧪

Зачем нужен @functools.wraps в декораторе?

🧪

Какой шаблон используется для декоратора с аргументами?

🧪

В каком порядке применяются стекированные декораторы @A @B def f(): ...?

🧪

Что делает @functools.singledispatch?