Декораторы: создание и применение обёрток для функций

Что такое декоратор и зачем он нужен? 🤔

Декоратор — это "обёртка" вокруг функции, которая изменяет или дополняет её поведение без изменения исходного кода. Это как супергеройский костюм для вашей функции! 💫

Где применяются декораторы:

  • Логирование работы функций
  • Замер времени выполнения
  • Кеширование результатов
  • Проверка прав доступа
  • Обработка ошибок и повторные попытки

Как выглядит простейший декоратор? 🔍

Рассмотрим базовый пример:

def my_decorator(func):
    def wrapper():
        print("Что-то происходит перед вызовом функции")
        func()
        print("Что-то происходит после вызова функции")
    return wrapper

@my_decorator
def say_hello():
    print("Привет!")

say_hello()

Вывод:

Что-то происходит перед вызовом функции
Привет!
Что-то происходит после вызовом функции

👉 @my_decorator — это синтаксический сахар для say_hello = my_decorator(say_hello)


Декораторы с аргументами функции 🎛️

Что если наша функция принимает параметры? Используем *args и **kwargs:

def log_arguments(func):
    def wrapper(*args, **kwargs):
        print(f"Вызов функции {func.__name__} с аргументами: {args}, {kwargs}")
        return func(*args, **kwargs)
    return wrapper

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

print(add(2, 3))

Вывод:

Вызов функции add с аргументами: (2, 3), {}
5

Возвращаем значения из декорируемых функций 🔄

Важно не забыть вернуть результат оборачиваемой функции!

def uppercase(func):
    def wrapper(*args, **kwargs):
        original_result = func(*args, **kwargs)
        modified_result = original_result.upper()
        return modified_result
    return wrapper

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

print(greet("Данила"))

Вывод:

ПРИВЕТ, ДАНИЛА!

Практический пример: замер времени выполнения ⏱️

Создадим полезный декоратор для профилирования кода:

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.perf_counter()
        result = func(*args, **kwargs)
        end_time = time.perf_counter()
        print(f"Функция {func.__name__!r} выполнена за {end_time - start_time:.4f} сек.")
        return result
    return wrapper

@timer
def calculate_something(n):
    return sum(i * i for i in range(n))

calculate_something(1000000)

Пример вывода:

Функция 'calculate_something' выполнена за 0.1253 сек.

Цепочка декораторов: несколько обёрток 🎁

Декораторы можно накладывать друг на друга! Порядок имеет значение:

def bold(func):
    def wrapper(*args, **kwargs):
        return f"<b>{func(*args, **kwargs)}</b>"
    return wrapper

def italic(func):
    def wrapper(*args, **kwargs):
        return f"<i>{func(*args, **kwargs)}</i>"
    return wrapper

@bold
@italic
def hello(name):
    return f"Привет, {name}"

print(hello("Мир"))

Вывод:

<b><i>Привет, Мир</i></b>

👉 Обратите внимание: порядок декораторов соответствует порядку оборачивания (bold применяется последним).


Декораторы с параметрами 🛠️

Иногда нужно передавать параметры самому декоратору. Для этого создаём "декоратор декораторов":

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

@repeat(num_times=3)
def greet(name):
    print(f"Привет, {name}!")

greet("Алексей")

Вывод:

Привет, Алексей!
Привет, Алексей!
Привет, Алексей!

Сохраняем метаданны функции с @functools.wraps 🏷️

При использовании декораторов теряется исходная информация о функции. Исправим это:

import functools

def debug(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Вызов {func.__name__} с {args}, {kwargs}")
        return func(*args, **kwargs)
    return wrapper

@debug
def add(a, b):
    """Складывает два числа"""
    return a + b

print(add.__name__)  # 'add' (было бы 'wrapper' без functools.wraps)
print(add.__doc__)   # 'Складывает два числа'

Реальный пример: кеширование результатов 🗃️

Реализуем мемоизацию с помощью декоратора:

from functools import wraps

def cache(func):
    memo = {}

    @wraps(func)
    def wrapper(*args):
        if args in memo:
            print(f"Возвращаем кешированный результат для {args}")
            return memo[args]
        result = func(*args)
        memo[args] = result
        return result
    return wrapper

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

print(fibonacci(10))

👉 Этот декоратор ускоряет рекурсивные вычисления в разы!


Когда использовать декораторы? 🎯

  1. DRY (Don't Repeat Yourself) — выносите повторяющийся код в декораторы
  2. Разделение ответственности — бизнес-логика и вспомогательный код разделены
  3. Расширяемость — можно добавлять новую функциональность без изменения исходного кода

Помните: декораторы должны делать код чище, а не сложнее! Если обёрток становится слишком много — возможно, стоит пересмотреть архитектуру.

Скрыть рекламу навсегда

🎥 YouTube: программирование простым языком

Канал, где я спокойно и по шагам объясняю сложные темы — без заумных терминов и лишней теории.

Подходит, если раньше «не заходило», но хочется наконец понять.

▶️ Смотреть курсы на YouTube