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

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

Основные преимущества декораторов

Базовое использование декораторов

Декораторы в Python используют синтаксис @имя_декоратора перед определением функции:

Простой декоратор
# Простой декоратор, который выводит сообщение до и после выполнения функции
def декоратор_простой(функция):
    def обертка(*args, **kwargs):
        print("Вызов функции:", функция.__name__)
        результат = функция(*args, **kwargs)
        print("Функция завершена:", функция.__name__)
        return результат
    return обертка

# Применение декоратора
@декоратор_простой
def приветствие(имя):
    print(f"Привет, {имя}!")

@декоратор_простой
def сложение(a, b):
    return a + b

# Вызов декорированных функций
приветствие("Анна")
результат = сложение(5, 3)
print(f"Результат сложения: {результат}")

# Эквивалент без использования синтаксиса @
def умножение(a, b):
    return a * b

# Ручное применение декоратора
умножение = декоратор_простой(умножение)
результат2 = умножение(4, 7)
print(f"Результат умножения: {результат2}")

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

Декораторы могут принимать параметры, что делает их более гибкими и мощными:

Декораторы с параметрами
# Декоратор с параметрами для повторения выполнения функции
def повторить(количество_раз):
    def декоратор_повтора(функция):
        def обертка(*args, **kwargs):
            результаты = []
            for i in range(количество_раз):
                print(f"Вызов #{i+1}:")
                результат = функция(*args, **kwargs)
                результаты.append(результат)
            return результаты
        return обертка
    return декоратор_повтора

# Применение декоратора с параметрами
@повторить(3)
def бросить_кубик():
    import random
    return random.randint(1, 6)

# Вызов функции, которая будет выполнена 3 раза
результаты = бросить_кубик()
print(f"Результаты бросков: {результаты}")

# Декоратор для ограничения количества вызовов функции
def ограничить_вызовы(максимум_вызовов):
    def декоратор_ограничения(функция):
        функция.вызовы = 0
        def обертка(*args, **kwargs):
            if функция.вызовы >= максимум_вызовов:
                raise RuntimeError(f"Функция {функция.__name__} может быть вызвана максимум {максимум_вызовов} раз")
            функция.вызовы += 1
            return функция(*args, **kwargs)
        return обертка
    return декоратор_ограничения

# Применение декоратора ограничения вызовов
@ограничить_вызовы(2)
def защищенная_функция():
    return "Доступ разрешен"

# Попытка вызвать функцию несколько раз
try:
    print(защищенная_функция())
    print(защищенная_функция())
    print(защищенная_функция())  # Это вызовет ошибку
except RuntimeError as e:
    print(f"Ошибка: {e}")

Классовые декораторы

Декораторы могут быть реализованы не только как функции, но и как классы:

Классовые декораторы
# Классовый декоратор для подсчета вызовов функции
class СчетчикВызовов:
    def __init__(self, функция):
        self.функция = функция
        self.количество_вызовов = 0
    
    def __call__(self, *args, **kwargs):
        self.количество_вызовов += 1
        print(f"Функция {self.функция.__name__} вызвана {self.количество_вызовов} раз")
        return self.функция(*args, **kwargs)

# Применение классового декоратора
@СчетчикВызовов
def приветствовать(имя):
    return f"Привет, {имя}!"

# Вызов функции несколько раз
print(приветствовать("Иван"))
print(приветствовать("Мария"))
print(приветствовать("Алексей"))

# Классовый декоратор с параметрами
class КэшированиеРезультатов:
    def __init__(self, функция):
        self.функция = функция
        self.кэш = {}
    
    def __call__(self, *args):
        if args in self.кэш:
            print(f"Возвращаем кэшированный результат для {args}")
            return self.кэш[args]
        else:
            print(f"Вычисляем результат для {args}")
            результат = self.функция(*args)
            self.кэш[args] = результат
            return результат

# Применение классового декоратора для кэширования
@КэшированиеРезультатов
def факториал(n):
    if n <= 1:
        return 1
    return n * факториал(n - 1)

# Вызов функции факториала
print(факториал(5))
print(факториал(3))  # Частично кэширован
print(факториал(5))  # Полностью кэширован

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

Декораторы находят широкое применение в реальных проектах для различных задач:

Логирование и измерение времени выполнения
import time
import functools

# Декоратор для логирования вызовов функций
def логировать(функция):
    @functools.wraps(функция)
    def обертка(*args, **kwargs):
        print(f"Вызов функции {функция.__name__} с аргументами {args} и {kwargs}")
        начало = time.time()
        результат = функция(*args, **kwargs)
        конец = time.time()
        print(f"Функция {функция.__name__} завершена за {конец - начало:.4f} секунд")
        return результат
    return обертка

# Декоратор для измерения времени выполнения
def измерить_время(функция):
    @functools.wraps(функция)
    def обертка(*args, **kwargs):
        начало = time.perf_counter()
        результат = функция(*args, **kwargs)
        конец = time.perf_counter()
        print(f"{функция.__name__} заняла {конец - начало:.6f} секунд")
        return результат
    return обертка

# Применение декораторов
@логировать
@измерить_время
def вычислить_сумму(n):
    return sum(range(n))

# Вызов функции с несколькими декораторами
результат = вычислить_сумму(1000000)
print(f"Сумма: {результат}")

# Декоратор для проверки типов аргументов
def проверить_типы(**типы_аргументов):
    def декоратор_проверки(функция):
        @functools.wraps(функция)
        def обертка(*args, **kwargs):
            # Получаем имена параметров функции
            сигнатура = functools.signature(функция)
            привязанные_аргументы = сигнатура.bind(*args, **kwargs)
            привязанные_аргументы.apply_defaults()
            
            # Проверяем типы
            for имя_аргумента, значение in привязанные_аргументы.arguments.items():
                if имя_аргумента in типы_аргументов:
                    ожидаемый_тип = типы_аргументов[имя_аргумента]
                    if not isinstance(значение, ожидаемый_тип):
                        raise TypeError(f"Аргумент '{имя_аргумента}' должен быть типа {ожидаемый_тип.__name__}")
            
            return функция(*args, **kwargs)
        return обертка
    return декоратор_проверки

# Применение декоратора проверки типов
@проверить_типы(a=int, b=int)
def умножить(a, b):
    return a * b

# Корректный вызов
print(умножить(5, 3))

# Некорректный вызов (вызовет ошибку)
try:
    умножить("5", 3)
except TypeError as e:
    print(f"Ошибка: {e}")
Декораторы для работы с исключениями
# Декоратор для повторных попыток выполнения функции при ошибках
def повторить_при_ошибке(максимум_попыток=3, задержка=1):
    def декоратор_повтора(функция):
        @functools.wraps(функция)
        def обертка(*args, **kwargs):
            for попытка in range(максимум_попыток):
                try:
                    return функция(*args, **kwargs)
                except Exception as e:
                    if попытка == максимум_попыток - 1:
                        raise e
                    print(f"Попытка {попытка + 1} не удалась: {e}. Повтор через {задержка} секунд...")
                    time.sleep(задержка)
        return обертка
    return декоратор_повтора

# Декоратор для игнорирования исключений
def игнорировать_исключения(*типы_исключений, значение_по_умолчанию=None):
    def декоратор_игнорирования(функция):
        @functools.wraps(функция)
        def обертка(*args, **kwargs):
            try:
                return функция(*args, **kwargs)
            except типы_исключений as e:
                print(f"Исключение проигнорировано: {e}")
                return значение_по_умолчанию
        return обертка
    return декоратор_игнорирования

# Пример функции, которая может вызывать ошибки
import random

@повторить_при_ошибке(максимум_попыток=5, задержка=0.5)
def ненадежная_функция():
    if random.random() < 0.7:  # 70% шанс ошибки
        raise ConnectionError("Ошибка подключения")
    return "Успешное выполнение"

# Вызов ненадежной функции
try:
    результат = ненадежная_функция()
    print(f"Результат: {результат}")
except ConnectionError as e:
    print(f"Все попытки неудачны: {e}")

@игнорировать_исключения(ZeroDivisionError, ValueError, значение_по_умолчанию=0)
def деление_с_защитой(a, b):
    return a / b

# Вызов функции с обработкой исключений
print(деление_с_защитой(10, 2))   # Нормальное выполнение
print(деление_с_защитой(10, 0))   # ZeroDivisionError будет проигнорирован
print(деление_с_защитой("10", 2))  # ValueError будет проигнорирован

Продвинутые возможности декораторов

Рассмотрим более сложные примеры использования декораторов:

Декораторы для методов классов
# Декоратор для проверки прав доступа
def требует_прав(права):
    def декоратор_прав(метод):
        @functools.wraps(метод)
        def обертка(self, *args, **kwargs):
            if not hasattr(self, 'права') or права not in self.права:
                raise PermissionError(f"Требуются права: {права}")
            return метод(self, *args, **kwargs)
        return обертка
    return декоратор_прав

# Декоратор для кэширования результатов методов
def кэшировать_метод(метод):
    кэш = {}
    @functools.wraps(метод)
    def обертка(self, *args, **kwargs):
        ключ_кэша = str(args) + str(sorted(kwargs.items()))
        if ключ_кэша not in кэш:
            кэш[ключ_кэша] = метод(self, *args, **kwargs)
        return кэш[ключ_кэша]
    return обертка

# Пример класса с декорированными методами
class Пользователь:
    def __init__(self, имя, права):
        self.имя = имя
        self.права = права
    
    @требует_прав("читать")
    @кэшировать_метод
    def получить_данные(self, идентификатор):
        print(f"Загрузка данных для {идентификатор}...")
        # Имитация загрузки данных
        time.sleep(1)
        return f"Данные пользователя {self.имя}, идентификатор {идентификатор}"
    
    @требует_прав("писать")
    def сохранить_данные(self, данные):
        print(f"Сохранение данных: {данные}")
        return "Данные сохранены"

# Создание пользователей с разными правами
админ = Пользователь("Админ", ["читать", "писать"])
гость = Пользователь("Гость", ["читать"])

# Вызов методов с правами
try:
    print(админ.получить_данные("123"))
    print(админ.получить_данные("123"))  # Из кэша
    print(админ.сохранить_данные("новые данные"))
except PermissionError as e:
    print(f"Ошибка доступа: {e}")

try:
    print(гость.сохранить_данные("попытка записи"))  # Нет прав
except PermissionError as e:
    print(f"Ошибка доступа: {e}")

Упражнения

Упражнение 1: Декоратор для ограничения частоты вызовов

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

import time
import functools

def ограничить_частоту(интервал_секунд):
    def декоратор_ограничения(функция):
        последний_вызов = 0
        @functools.wraps(функция)
        def обертка(*args, **kwargs):
            nonlocal последний_вызов
            текущее_время = time.time()
            if текущее_время - последний_вызов < интервал_секунд:
                оставшееся_время = интервал_секунд - (текущее_время - последний_вызов)
                raise RuntimeError(f"Функцию можно вызывать не чаще чем раз в {интервал_секунд} секунд. Осталось ждать {оставшееся_время:.2f} секунд")
            последний_вызов = текущее_время
            return функция(*args, **kwargs)
        return обертка
    return декоратор_ограничения

# Тестирование
@ограничить_частоту(2)  # Ограничение: 1 вызов в 2 секунды
def запрос_к_api():
    return "Данные от API"

# Попытка частых вызовов
try:
    print(запрос_к_api())
    print(запрос_к_api())  # Должен вызвать ошибку
except RuntimeError as e:
    print(f"Ошибка: {e}")

print("Ждем 2 секунды...")
time.sleep(2)
print(запрос_к_api())  # Должен выполниться успешно
Упражнение 2: Декоратор для валидации аргументов

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

import functools

def только_положительные(функция):
    @functools.wraps(функция)
    def обертка(*args, **kwargs):
        # Проверяем позиционные аргументы
        for аргумент in args:
            if not isinstance(аргумент, (int, float)) or аргумент <= 0:
                raise ValueError(f"Все аргументы должны быть положительными числами. Получено: {аргумент}")
        
        # Проверяем именованные аргументы
        for имя, значение in kwargs.items():
            if not isinstance(значение, (int, float)) or значение <= 0:
                raise ValueError(f"Аргумент '{имя}' должен быть положительным числом. Получено: {значение}")
        
        return функция(*args, **kwargs)
    return обертка

# Тестирование
@только_положительные
def вычислить_площадь_прямоугольника(длина, ширина):
    return длина * ширина

# Корректные вызовы
print(вычислить_площадь_прямоугольника(5, 3))
print(вычислить_площадь_прямоугольника(длина=4, ширина=6))

# Некорректные вызовы
try:
    вычислить_площадь_прямоугольника(-5, 3)
except ValueError as e:
    print(f"Ошибка: {e}")

try:
    вычислить_площадь_прямоугольника(5, ширина=0)
except ValueError as e:
    print(f"Ошибка: {e}")
Упражнение 3: Декоратор для преобразования возвращаемого значения

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

import functools

def в_верхний_регистр(функция):
    @functools.wraps(функция)
    def обертка(*args, **kwargs):
        результат = функция(*args, **kwargs)
        if isinstance(результат, str):
            return результат.upper()
        return результат
    return обертка

# Тестирование
@в_верхний_регистр
def приветствие(имя):
    return f"привет, {имя}!"

@в_верхний_регистр
def сложение(a, b):
    return a + b

# Вызов функций
print(приветствие("анна"))  # Будет в верхнем регистре
print(сложение(5, 3))      # Останется числом

@в_верхний_регистр
def получить_статус():
    return "операция выполнена успешно"

print(получить_статус())  # Будет в верхнем регистре
Предыдущий урок Следующий урок