Паттерн Декоратор в Python

Основная идея паттерна декоратор — обёртывание: создавая объект-обёртку (декоратор), который оборачивает исходный объект, тем самым расширяя его функциональность без изменения исходного объекта.

Базовые понятия

Паттерн декоратор состоит из четырёх основных ролей:

  1. Интерфейс компонента (Component): определяет общий интерфейс для декорируемого объекта и декоратора
  2. Конкретный компонент: исходный объект, который нужно декорировать
  3. Базовый класс декоратора (Decorator): хранит ссылку на объект компонента и реализует интерфейс компонента
  4. Конкретный декоратор: реализует конкретную функциональность декорирования


Базовый синтаксис декораторов

Декораторы функций

Декораторы функций — самая распространённая форма декораторов. Они принимают функцию в качестве аргумента и возвращают новую функцию.

Пример

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Некоторые операции перед выполнением функции")
        result = func(*args, **kwargs)
        print("Некоторые операции после выполнения функции")
        return result
    return wrapper

@my_decorator
def say_hello(name):
    print(f"Привет, {name}!")

# Использовать декоратор
say_hello("Алиса")

Вывод:

函数执行前的一些操作
Hello, Alice!
函数执行后的一些操作

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

Если вам нужно передать параметры в декоратор, нужно вложить еще один уровень функций:

Пример

def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(times):
                print(f"Execution {i+1}:")
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

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

greet("Bob")

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

Помимо декораторов функций, Python также поддерживает декораторы классов. Декораторы классов работают путем реализации__call__методов для работы.

Базовый декоратор класса

Пример

class TimerDecorator:
    def __init__(self, func):
        self.func = func
   
    def __call__(self, *args, **kwargs):
        import time
        start_time = time.time()
        result = self.func(*args, **kwargs)
        end_time = time.time()
        print(f"Функция {self.func.__name__} выполнена за {end_time - start_time:.4f} секунд")
        return result

@TimerDecorator
def calculate_sum(n):
    return sum(range(n))

result = calculate_sum(1000000)
print(f"Результат вычисления: {result}")

Декоратор-класс с параметрами

Пример

class LogDecorator:
    def __init__(self, level="INFO"):
        self.level = level
   
    def __call__(self, func):
        def wrapper(*args, **kwargs):
            print(f"[{self.level}] Вызов функции: {func.__name__}")
            print(f"[{self.level}] Аргументы: args={args}, kwargs={kwargs}")
            result = func(*args, **kwargs)
            print(f"[{self.level}] Возвращаемое значение: {result}")
            return result
        return wrapper

@LogDecorator(level="DEBUG")
def multiply(a, b):
    return a * b

multiply(5, 3)

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

Python предоставляет несколько полезных встроенных декораторов:

@staticmethod и @classmethod

Пример

class Calculator:
    @staticmethod
    def add(x, y):
        return x + y
   
    @classmethod
    def multiply(cls, x, y):
        return x * y

# Использовать статический метод
result1 = Calculator.add(5, 3)
print(f"Результат статического метода: {result1}")

# Использовать метод класса
result2 = Calculator.multiply(5, 3)
print(f"Результат метода класса: {result2}")

@property

Пример

class Circle:
    def __init__(self, radius):
        self._radius = radius
   
    @property
    def radius(self):
        return self._radius
   
    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("Радиус должен быть положительным числом")
        self._radius = value
   
    @property
    def area(self):
        return 3.14159 * self._radius ** 2

circle = Circle(5)
print(f"Радиус: {circle.radius}")
print(f"Площадь: {circle.area}")

circle.radius = 10
print(f"Новый радиус: {circle.radius}")
print(f"Новая площадь: {circle.area}")

Практические сценарии применения декораторов

1. Логирование

Пример

def log_execution(func):
    def wrapper(*args, **kwargs):
        print(f"Начало выполнения: {func.__name__}")
        try:
            result = func(*args, **kwargs)
            print(f"Успешно завершено: {func.__name__}")
            return result
        except Exception as e:
            print(f"Выполнение не удалось: {func.__name__}, ошибка: {e}")
            raise
    return wrapper

@log_execution
def process_data(data):
    # Имитация обработки данных
    if not data:
        raise ValueError("Данные не могут быть пустыми")
    return [x * 2 for x in data]

#
data = [1, 2, 3]
result = process_data(data)
print(f"Результат обработки: {result}")

2. Проверка разрешений

Пример

def require_login(func):
    def wrapper(user, *args, **kwargs):
        if not user.get('is_authenticated', False):
            raise PermissionError("Пользователь не авторизован")
        return func(user, *args, **kwargs)
    return wrapper

@require_login
def view_profile(user):
    return f"Просмотр профиля пользователя {user['username']}"

#
user1 = {'username': 'alice', 'is_authenticated': True}
user2 = {'username': 'bob', 'is_authenticated': False}

print(view_profile(user1))  # Обычное выполнение
# print(view_profile(user2)) # вызовет PermissionError

3. Декоратор кэширования

Пример

def cache_results(func):
    cache = {}
   
    def wrapper(*args):
        if args in cache:
            print(f"Получаем результат из кэша: {args}")
            return cache[args]
        result = func(*args)
        cache[args] = result
        print(f"Вычисляем и кэшируем результат: {args} -> {result}")
        return result
    return wrapper

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

# Проверка эффекта кэширования
print(fibonacci(5))
print(fibonacci(5))  # На этот раз оно будет взято из кэша

Порядок выполнения нескольких декораторов

При использовании нескольких декораторов порядок их выполнения — снизу вверх:

Пример

def decorator1(func):
    def wrapper():
        print("Декоратор 1 - до")
        func()
        print("Декоратор 1 - после")
    return wrapper

def decorator2(func):
    def wrapper():
        print("Декоратор 2 - до")
        func()
        print("Декоратор 2 - после")
    return wrapper

@decorator1
@decorator2
def my_function():
    print("Original function")

my_function()

Вывод:

装饰器1 - 前
装饰器2 - 前
原始函数
装饰器2 - 后
装饰器1 - 后

Сохранение метаданных функции

При использовании декораторов метаданные исходной функции (такие как имя функции, строка документации и т. д.) перезаписываются функцией-обёрткой. Вы можете использоватьfunctools.wrapsчтобы сохранить эту информацию:

Пример

from functools import wraps

def preserve_metadata(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """Docstring of the wrapper function"""
        return func(*args, **kwargs)
    return wrapper

@preserve_metadata
def example_function():
    """Это строка документации исходной функции"""
    pass

print(f"Имя функции: {example_function.__name__}")
print(f"Строка документации: {example_function.__doc__}")

Практические упражнения

Упражнение 1: Создание декоратора мониторинга производительности

Напишите декоратор для мониторинга времени выполнения и использования памяти функции.

Пример

import time
import tracemalloc

def performance_monitor(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Начать отслеживание памяти
        tracemalloc.start()
       
        # Записать время начала
        start_time = time.time()
       
        # Выполнить функцию
        result = func(*args, **kwargs)
       
        # Записать время окончания
        end_time = time.time()
       
        # Получить использование памяти
        current, peak = tracemalloc.get_traced_memory()
        tracemalloc.stop()
       
        print(f"Отчёт о производительности для функции {func.__name__}:")
        print(f"Время выполнения: {end_time - start_time:.4f} секунд")
        print(f"Использование памяти: текущее {current/1024:.2f} KB, пиковое {peak/1024:.2f} KB")
       
        return result
    return wrapper

@performance_monitor
def process_large_data():
    """Имитация обработки больших объёмов данных"""
    data = [i**2 for i in range(100000)]
    return sum(data)

process_large_data()

Упражнение 2: Реализация декоратора механизма повторных попыток

Создайте декоратор, который автоматически выполняет повторные попытки заданное число раз при сбое выполнения функции.

Пример

def retry(max_attempts=3, delay=1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_attempts:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    attempts += 1
                    if attempts == max_attempts:
                        print(f"Функция {func.__name__} завершилась с ошибкой, достигнуто максимальное количество повторных попыток")
                        raise
                    print(f"Функция {func.__name__} завершилась с ошибкой при попытке {attempts}: {e}")
                    print(f"Ожидание {delay} секунд перед повторной попыткой...")
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

@retry(max_attempts=3, delay=2)
def unstable_operation():
    """Имитирует нестабильную операцию"""
    import random
    if random.random() < 0.7:  # Вероятность сбоя 70%
        raise ValueError("Случайный сбой")
    return "Операция выполнена успешно"

# Проверка механизма повторных попыток
result = unstable_operation()
print(f"Итоговый результат: {result}")

Итог

Декораторы Python — мощный и гибкий инструмент, который обеспечивает следующие преимущества:

  1. Повторное использование кода: Отделяет сквозные аспекты (такие как логирование, кэширование, валидация) от бизнес-логики
  2. Динамическое расширение: Добавляет новую функциональность без изменения исходного кода
  3. Соблюдайте краткость: Используйте@синтаксис делает код более ясным и читаемым
  4. Следует принципу открытости/закрытости: Открыт для расширения, закрыт для изменения

Лучшие практики использования декораторов:

  • Использованиеfunctools.wrapsСохраняйте метаданные функции
  • Соблюдайте принцип единственной ответственности декоратора
  • Правильно обрабатывайте исключения в декораторах
  • Обращайте внимание на порядок выполнения декораторов
Другие расширения