Введение. Зачем вообще нужны декораторы?
Представьте типичную ситуацию: вы написали отличный кусок кода. У вас есть десяток функций, которые делают полезную работу — например, ходят в стороннее API, парсят данные и аккуратно складывают их в базу. Код читаемый, лаконичный, всё работает как часы.
И тут приходит тимлид (или заказчик) и говорит: «Слушай, а давай-ка мы будем замерять время выполнения каждой функции и писать это в логи. Ну, чтобы отлавливать тормоза на проде».
Что делает разработчик, который не знает про декораторы? Он тяжело вздыхает и идёт руками вписывать в каждую функцию что-то вроде этого:
import time def fetch_data(): start_time = time.time() # Начали замер # ... тут 50 строк вашего полезного кода ... end_time = time.time() print(f"Функция fetch_data выполнялась {end_time - start_time} секунд") # Закончили замер return data
А теперь представьте, что таких функций у вас не две, а пятьдесят.
Во-первых, писать это безумно скучно. Во-вторых, вы грубо нарушаете главный принцип разработчика — DRY (Don't Repeat Yourself, не повторяйся). В-третьих, ваш чистый бизнес-код только что оброс служебным мусором. А если завтра попросят поменять формат вывода времени? Придется бегать по всему проекту и менять print во всех пятидесяти местах.
Вот именно эту проблему и призваны решать декораторы. Они идеально подходят для так называемого сквозного функционала — вещей, которые не относятся к самой сути функции, но должны происходить вокруг нее (логирование, кэширование, проверка прав доступа, повторные по��ытки при ошибке сети).
Если вы хотите навсегда забыть о дублировании кода, понять, как устроена эта «магия» под капотом, и научиться писать элегантные решения — добро пожаловать на мой бесплатный курс «Декораторы в Python – от основ до практического применения». Там мы шаг за шагом разберем всё: от простейших оберток до сложных декораторов с аргументами для реальных проектов.
2. Фундамент: функции — это объекты первого класса
Синтаксис с «собачкой» (@) часто кажется новичкам какой-то встроенной магией языка. Но под капотом нет никакого волшебства. Чтобы понять, как работают декораторы, нужно осознать всего одну концепцию: в Python функции — это объекты первого класса (first-class citizens).
Звучит как термин из учебника по информатике, но на практике это означает очень простую вещь: функции в Питоне ничем не отличаются от обычных чисел, строк или списков. С ними можно делать всё то же самое!
Давайте посмотрим на три главных фокуса, которые можно проворачивать с функциями.
Фокус первый: функцию можно положить в переменную
Обычно мы вызываем функцию по её имени. Но ничто не мешает нам дать ей псевдоним:
def greet(): print("Привет, Хабр!") # Присваиваем функцию переменной. # ВАЖНО: мы пишем имя без скобок! Мы не вызываем функцию, мы ее передаем. say_hello = greet # Теперь say_hello делает то же самое, что и greet say_hello() # Вывод: Привет, Хабр!
Если бы мы написали greet(), то в переменную записался бы результат выполнения функции (в данном случае None). А написав просто greet, мы передали сам объект функции.
Фокус второй: функцию можно передать как аргумент
Если функция — это такой же объект, как строка "hello" или число 42, значит, мы можем передать её внутрь другой функции!
def shout(): print("УРААА!") def whisper(): print("тссс...") # Эта функция принимает ДРУГУЮ функцию в качестве аргумента def execute_twice(func): func() # Вызываем переданную функцию первый раз func() # И второй раз execute_twice(shout) # Вывод: # УРААА! # УРААА! execute_twice(whisper) # Вывод: # тссс... # тссс...
Функции, которые принимают другие функции (или возвращают их), называются функциями высшего порядка. Декораторы — это как раз они и есть.
Фокус третий: матрешка из функций (вложенность и возврат)
Мы подошли к самому главному. В Python можно создавать функцию внутри другой функции. И самое классное — внешняя функция может вернуть внутреннюю как результат своей работы!
Смотрите внимательно на этот пример:
def make_multiplier(n): # Создаем внутреннюю функцию def multiplier(x): return x * n # Возвращаем саму функцию, а не результат ее работы! (без скобок) return multiplier # Создаем функцию, которая всегда умножает на 3 times_three = make_multiplier(3) print(times_three(10)) # Выведет 30 print(times_three(5)) # Выведет 15
Здесь происходит кое-что очень важное: концепция замыкания (closure).
Внутренняя функция multiplier запомнила значение переменной n из внешней функции make_multiplier, даже когда внешняя функция уже завершила свою работу!
Собираем пазл
Давайте еще раз посмотрим на этот арсенал:
Мы можем передать функцию как аргумент.
Мы можем создать внутри новую функцию (которая запомнит всё, что было снаружи).
Мы можем вернуть эту новую функцию.
Если соединить эти три шага вместе... Бинго! Мы получим рецепт создания любого декоратора в Python.
3. Пишем свой первый декоратор
Давайте напишем простейший декоратор. Пусть он делает очень простую вещь: здоровается перед тем, как выполнить основную функцию, и прощается после её завершения.
Нам нужна функция, которая принимает функцию, создает внутри себя функцию-обертку и возвращает эту самую обертку. Звучит как скороговорка, но в коде это выглядит очень логично:
def polite_decorator(func): # Создаем функцию-обертку def wrapper(): print("Здравствуйте! Начинаем работу.") func() # Вызываем оригинальную функцию print("Работа завершена. До свидания!") # Возвращаем обертку (без скобок!) return wrapper
Вот и всё, наш декоратор готов! Теперь давайте напишем какую-нибудь обычную полезную функцию:
def do_work(): print("Пилю фичи, фиксим баги...")
Применяем декоратор «вручную» (без магии)
Как нам теперь соединить polite_decorator и do_work? Мы знаем, что функции можно передавать как аргументы и присваивать переменным:
# Передаем оригинальную функцию в декоратор и перезаписываем её же имя! do_work = polite_decorator(do_work) # Вызываем функцию do_work()
Что выведет консоль?
Здравствуйте! Начинаем работу. Пилю фичи, фиксим баги... Работа завершена. До свидания!
Что только что произошло под капотом?
Мы взяли оригинальную функцию do_work, отдали её в polite_decorator. Декоратор запаковал её внутрь своей функции wrapper и вернул этот wrapper наружу. Затем мы взяли этот wrapper и присвоили его переменной с именем do_work.
Оригинальная функция никуда не исчезла, она надежно спрятана (замкнута) внутри обертки. Но теперь, когда весь остальной код в нашем проекте попытается вызвать do_work(), он на самом деле будет вызывать wrapper().
Вот он — момент истины. Никакой магии, просто подмена понятий (и переменных)!
Встречайте: синтаксический сахар @
Писать конструкцию my_func = decorator(my_func) каждый раз после объявления функции — это, согласитесь, не очень красиво. Особенно если функция длинная, можно просто забыть применить декоратор в конце.
Поэтому создатели Python сжалили��ь над нами и добавили синтаксический сахар — тот самый символ @ («собачка» или pie-syntax).
Символ @имя_декоратора перед объявлением функции делает ровно то же самое: он автоматически берет функцию под ним, прогоняет через декоратор и перезаписывает имя.
Теперь наш код выглядит чисто и профессионально:
def polite_decorator(func): def wrapper(): print("Здравствуйте! Начинаем работу.") func() print("Работа завершена. До свидания!") return wrapper # Используем синтаксический сахар @polite_decorator def do_work(): print("Пилю фичи, фиксим баги...") do_work()
Результат будет абсолютно идентичным. Конструкция @polite_decorator — это просто красивая, короткая и общепринятая замена для строки do_work = polite_decorator(do_work).
4. Аргументы и возврат значений (Как не сломать оригинальную функцию)
Всё бы хорошо, но в реальном мире функции редко бывают такими примитивными, как наш do_work() из предыдущего примера. Обычно они принимают какие-то параметры и возвращают результат.
Давайте посмотрим, что произойдет, если мы применим наш текущий polite_decorator к полезной функции сложения двух чисел:
@polite_decorator def add_numbers(a, b): return a + b # Пробуем вызвать add_numbers(10, 20)
Запускаем код и ловим жирный красный трейсбек:
TypeError: wrapper() takes 0 positional arguments but 2 were given
Почему так вышло?
Вспоминаем магию из предыдущего раздела: вызывая add_numbers(10, 20), мы на самом деле вызываем функцию wrapper(10, 20). А как мы написали wrapper? Правильно, def wrapper(): — он вообще не принимает никаких аргументов!
Решение: универсальная обертка с *args и **kwargs
Нам нужно сделать так, чтобы wrapper мог проглотить любое количество аргументов — хоть ноль, хоть пять позиционных, хоть десяток именованных. И передать их все дальше, в оригинальную функцию.
Для этого в Питоне есть замечательный механизм распаковки: *args (для позиционных аргументов) и **kwargs (для именованных, передаваемых по ключу).
Чиним наш декоратор:
def polite_decorator(func): # Теперь обертка готова принять что угодно! def wrapper(*args, **kwargs): print("Здравствуйте! Начинаем работу.") # Передаем все собранные аргументы в оригинальную функцию func(*args, **kwargs) print("Работа завершена. До свидания!") return wrapper
Пробуем снова: add_numbers(10, 20). Ошибки нет, текст печатается. Ура? Не совсем.
Скрытая ловушка: куда пропал результат?
Давайте попробуем распечатать результат нашей математики:
result = add_numbers(10, 20) print(f"Результат сложения: {result}")
Вывод в консоли вас неприятно удивит:
Здравствуйте! Начинаем работу. Работа завершена. До свидания! Результат сложения: None
Стоп, почему None? 10 + 20 должно быть 30!
Давайте еще раз проследим путь выполнения внутри нашего обновленного wrapper:
Мы напечатали приветствие.
Мы вызвали
func(10, 20). Оригинальная функция честно посчитала 30 и вернула его.Но наш
wrapperникак не сохранил это значение! Он просто пошел дальше, напечатал прощание и завершил работу. А любая функция в Python, в которой нет явногоreturn, возвращаетNone.
Финальный штрих: возвращаем результат наружу
Чтобы не ломать логику программы, декоратор обязан быть «прозрачным». Он должен вернуть ровно то же самое, что вернула бы оригинальная функция без него.
Для этого нам нужно поймать результат работы func в переменную и вернуть её в самом конце wrapper:
def polite_decorator(func): def wrapper(*args, **kwargs): print("Здравствуйте! Начинаем работу.") # Ловим результат работы оригинальной функции result = func(*args, **kwargs) print("Работа завершена. До свидания!") # Честно отдаем его наружу return result return wrapper
Вот теперь наш add_numbers(10, 20) честно вернет 30.
Запомните этот шаблон!
Конструкция:
def wrapper(*args, **kwargs): # ... код до ... result = func(*args, **kwargs) # ... код после ... return result
— это золотой стандарт, скелет любого классического декоратора. Если вы усвоите этот паттерн, вы уже никогда не запутаетесь при написании собственных оберток.
5. Сохранение метаданных: почему вам жизненно необходим functools.wraps
Итак, наш декоратор из прошлой части работает идеально. Он проглатывает любые аргументы и честно возвращает результат. Казалось бы, можно расходиться?
Но есть одна скрытая проблема (ложка дёгтя), за которую старшие товарищи на код-ревью обязательно сделают вам замечание. Давайте посмотрим, в чем дело.
Напишем хорошую, правильную функцию с документацией (docstring) и обернем её нашим декоратором:
@polite_decorator def add_numbers(a, b): """Складывает два числа и возвращает результат.""" return a + b
А теперь давайте представим, что мы хотим узнать имя этой функции и прочитать её документацию через код (именно так делают различные анализаторы и генераторы документации):
print(add_numbers.__name__) # Печатаем имя функции print(add_numbers.__doc__) # Печатаем документацию
Ожидание:
add_numbers Складывает два числа и возвращает результат.
Реальность:
wrapper None
Что произошло?!
Вспоминаем механику: декоратор подменяет оригинальную функцию функцией wrapper. То есть add_numbers теперь ссылается на объект функции wrapper. А у нашего wrapper имя, логично, "wrapper", и никакой документации мы ему не писали (поэтому None).
Почему это вообще должно нас волновать?
Если вы пишете скрипт на 100 строк для себя — возможно, вам б��дет всё равно. Но в реальной разработке потеря метаданных — это катастрофа, и вот почему:
Отладка превращается в ад. Представьте, что у вас в проекте 50 функций обернуты декораторами, и где-то падает ошибка. Вы открываете логи (трейсбек), а там написано:
Error in function wrapper. В каком именно из 50wrapper'ов произошла ошибка? Удачи в поисках.Ломается генерация документации. Фреймворки вроде FastAPI или библиотеки вроде Sphinx (для генерации красивых доков) активно читают
__name__и__doc__ваших функций, чтобы построить красивый интерфейс (например, Swagger). Если все ваши API-эндпоинты будут называтьсяwrapperбез описания — API превратится в тыкву.
Решение: магия functools.wraps
К счастью, создатели Python предвидели эту проблему и положили в стандартную библиотеку элегантное решение — модуль functools.
Всё, что нам нужно сделать — это импортировать оттуда специальный декоратор wraps и применить его... к нашему wrapper'у! Да, это декоратор для декоратора.
Выглядит это так:
from functools import wraps def polite_decorator(func): @wraps(func) # <--- Указываем, чьи метаданные нужно скопировать def wrapper(*args, **kwargs): print("Здравствуйте! Начинаем работу.") result = func(*args, **kwargs) print("Работа завершена. До свидания!") return result return wrapper
Что делает @wraps(func)? Он берет оригинальную функцию func, аккуратно копирует из неё имя (__name__), документацию (__doc__) и другие полезные метаданные, и переносит их на наш wrapper.
Теперь, если мы снова вызовем:
print(add_numbers.__name__) print(add_numbers.__doc__)
Мы получим правильный результат: add_numbers и строку с документацией. Функция снова выглядит и ведет себя как оригинальная, сохранив при этом новую логику!
Золотой стандарт декоратора
То, что мы сейчас написали — это Абсолютный Золотой Стандарт написания декораторов-функций в Python. Запомните этот шаблон:
from functools import wraps def my_decorator(func): @wraps(func) def wrapper(*args, **kwargs): # ... что-то делаем ДО ... result = func(*args, **kwargs) # ... что-то делаем ПОСЛЕ ... return result return wrapper
6. Продвинутый уровень 1: Декораторы с аргументами
Мы с вами написали отличный «Золотой стандарт» декоратора. Но у него есть один недостаток — он статичный. Его поведение жестко зашито внутри кода.
Давайте представим реальную жизненную задачу. Вы пишете парсер или скрипт, который ходит по сети в нестабильное API. Сеть может моргнуть, сервер может ответить 502 ошибкой. Было бы здорово написать декоратор @retry, который в случае ошибки попробует выполнить функцию еще несколько раз, прежде чем окончательно упасть.
Если мы напишем обычный декоратор, нам придется жестко захардкодить количество попыток:
# Где-то внутри wrapper... for _ in range(3): # Жестко зашили 3 попытки try: return func(*args, **kwargs) except Exception: pass
А что, если для скачивания картинки нам хватит 3 попыток, а для тяжелого отчета из базы данных нужно 10? Писать @retry_3 и @retry_10? Это путь в никуда.
Нам хочется написать вот так:
@retry(times=5) def fetch_data(): ...
Как это работает: Фабрика декораторов
Чтобы понять, как написать декоратор с аргументами, нужно вспомнить, как работает синтаксический сахар @.
Обычно мы пишем @my_decorator. Питон берет функцию my_decorator и передает в нее нашу оборачиваемую функцию.
Но когда мы пишем @retry(times=5), происходит два шага:
Сначала Питон выполняет функцию
retry(times=5).Питон берет то, что вернула эта функция, и использует это как декоратор!
Значит, retry — это больше не декоратор. Это фабрика декораторов. Функция, которая создает и возвращает декоратор.
А раз так, нам понадобится добавить еще один, третий уровень матрешки!
Пишем декоратор с аргументами (3 уровня вложенности)
Давайте соберем нашего Франкенштейна. Нам нужны:
Внешняя функция (фабрика), которая принимает аргумент
times.Средняя функция (сам декоратор), которая принимает
func.Внутренняя функция (обертка/wrapper), которая принимает
*args, **kwargsи делает всю грязную работу.
Поехали:
from functools import wraps import time # Уровень 1: Фабрика. Принимает настройки декоратора. def retry(times=3, delay=1): # Уровень 2: Сам декоратор. Принимает функцию. def decorator(func): # Уровень 3: Обертка. Принимает аргументы функции. @wraps(func) def wrapper(*args, **kwargs): # Здесь нам доступны и параметры функции (*args), # и настройки из фабрики (times, delay)! for attempt in range(1, times + 1): try: return func(*args, **kwargs) except Exception as e: print(f"Ошибка: {e}. Попытка {attempt} из {times}...") time.sleep(delay) # Если цикл закончился, а return не сработал — вызываем ошибку print("Все попытки исчерпаны!") raise Exception("Функция не отработала") return wrapper # Декоратор возвращает обертку return decorator # Фабрика возвращает декоратор
Ух, целых три def и два return подряд! Выглядит громоздко, но давайте посмотрим, как изящно это работает на практике:
import random # Настраиваем декоратор: 5 попыток с паузой в 2 секунды @retry(times=5, delay=2) def unstable_network_call(): if random.random() < 0.8: # В 80% случаев функция падает raise ConnectionError("Сеть недоступна") return "Данные успешно получены!" # Запускаем! result = unstable_network_call() print(result)
Пример вывода в консоли:
Ошибка: Сеть недоступна. Попытка 1 из 5... Ошибка: Сеть недоступна. Попытка 2 из 5... Данные успешно получены!
Что происходит под капотом? (Развеиваем магию)
Если убрать синтаксический сахар @, то применение нашего декоратора с параметрами выглядело бы так:
# 1. Создаем конкретный декоратор под наши нужды my_custom_decorator = retry(times=5, delay=2) # 2. Применяем его к функции unstable_network_call = my_custom_decorator(unstable_network_call)
Или, если записать это в одну строчку:
unstable_network_call = retry(times=5, delay=2)(unstable_network_call)
Магия замыканий в Питоне (которую мы обсуждали во второй части) позволяет самой глубокой функции wrapper «помнить» значения times и delay, которые мы передали на самом верхнем уровне в retry.
Да, три уровня вложенности заставляют глаза немного кровоточить при первом знакомстве. Но как только вы напишете парочку таких декораторов своими руками, этот паттерн навсегда отпечатается у вас в памяти.
7. Продвинутый уровень 2: Декораторы на основе классов
Давайте будем честны: конструкция def-def-def из прошлого примера выглядит жутковато. Читать такой код тяжело, а отлаживать — еще тяжелее.
Но есть и другая проблема. Что, если нашему декоратору нужно что-то запоминать между вызовами функции? Например, мы хотим написать декоратор @counter, который будет считать, сколько раз функция была вызвана за время работы программы.
Делать это на обычных функциях неудобно: придется использовать глобальные переменные или странные хаки с атрибутами функций. И вот тут на сцену выходит Объектно-Ориентированное Программирование (ООП). Классы идеально подходят для того, чтобы хранить состояние (данные) вместе с поведением.
Секретное оружие: маг��ческий метод __call__
В Python декоратором может быть вообще что угодно, главное условие — это должно быть вызываемым объектом (callable). То есть тем, после чего можно поставить круглые скобочки ().
Функции вызываемы по своей природе. А как сделать вызываемым объект класса? Для этого в Python есть магический метод __call__. Если вы добавите его в свой класс, экземпляры этого класса можно будет вызывать точно так же, как функции!
Смотрите, как просто:
class MyCallableClass: def __call__(self): print("Меня вызвали как функцию!") obj = MyCallableClass() obj() # Вывод: Меня вызвали как функцию!
Пишем класс-декоратор (сохраняем состояние)
Теперь давайте напишем тот самый декоратор-счетчик @counter.
Как Питон работает с классом-декоратором без аргументов (вида @counter)?
Он передает оборачиваемую функцию в конструктор класса
__init__.Когда мы вызываем нашу функцию в коде, на самом деле вызывается метод
__call__созданного объекта.
Перекладываем это на код:
import functools class CallCounter: def __init__(self, func): self.func = func self.count = 0 # Здесь мы храним состояние (счетчик) # Аналог @wraps для классов, чтобы не потерять имя и докстринг functools.update_wrapper(self, func) def __call__(self, *args, **kwargs): self.count += 1 print(f"Функция {self.func.__name__} была вызвана {self.count} раз(а)") # Вызываем оригинальную функцию и возвращаем результат return self.func(*args, **kwargs)
Применяем:
@CallCounter def say_hi(name): print(f"Привет, {name}!") say_hi("Алексей") say_hi("Анна") say_hi("Иван")
Вывод:
Функция say_hi была вызвана 1 раз(а) Привет, Алексей! Функция say_hi была вызвана 2 раз(а) Привет, Анна! Функция say_hi была вызвана 3 раз(а) Привет, Иван!
Согласитесь, выглядит гораздо чище, чем матрешка из функций? Все данные лежат в self, логика разделена по методам. Красота!
Как подружить класс-декоратор с аргументами
А вот теперь внимание, ловушка.
Если мы хотим передать в наш класс-декоратор аргументы (например, @retry(times=3)), правила игры меняются!
Вспоминаем, как Питон читает строку @retry(times=3):
Сначала он выполняет retry(times=3). Если retry — это класс, то создается экземпляр класса, и в его __init__ попадают наши аргументы (times=3), а вовсе не функция!
Затем Питон берет этот созданный экземпляр и вызывает его, передавая ему функцию. То есть функция прилетает в __call__.
Роли поменялись! Давайте перепишем наш @retry из предыдущей главы в виде класса:
from functools import wraps import time class Retry: # 1. Сюда прилетают настройки декоратора def __init__(self, times=3, delay=1): self.times = times self.delay = delay # 2. Сюда прилетает сама функция def __call__(self, func): # 3. А здесь мы создаем привычный wrapper @wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, self.times + 1): try: return func(*args, **kwargs) except Exception as e: print(f"Ошибка. Попытка {attempt} из {self.times}...") time.sleep(self.delay) raise Exception("Все попытки исчерпаны!") return wrapper
Почему здесь снова появилась функция wrapper внутри __call__?
Потому что метод __call__ теперь играет роль фабрики. Он принял функцию func и должен вернуть её обернутую версию. Если бы мы написали логику прямо внутри __call__ (как в примере со счетчиком), то функция вызвалась бы мгновенно в момент декорирования, а не тогда, когда мы этого хотим.
Что выбрать: функции или классы?
Нет строгого правила, но в сообществе сложился такой консенсус:
Если ваш декоратор простой и просто меняет ввод/вывод функции — используйте функции. Это быстрее пишется и привычнее читается.
Если декоратор сложный, должен хранить в себе какие-то данные (кэш, счетчики, подключения к БД) или имеет много вспомогательных методов — используйте классы. ООП поможет навести в этом порядок.
8. Боевые примеры из жизни (Real-world cases)
Хватит синтетических примеров со сложением чисел и печатью приветствий. Давайте посмотрим, где декораторы приносят реальную, осязаемую пользу и экономят часы дебага. Вот 4 паттерна, которые регулярно встречаются в боевых проектах.
1. Таймер (@benchmark): ищем «тормоза» в коде
Самый классический пример. Когда ваш скрипт начинает работать подозрительно долго, нужно быстро понять, какая именно функция тупит. Вместо того чтобы засорять весь проект модулем time, мы пишем одну элегантную обертку.
Небольшой совет: для замеров времени выполнения лучше использовать time.perf_counter(), а не time.time(), так как он точнее и не зависит от перевода системных часов.
import time from functools import wraps def benchmark(func): """Декоратор для замера времени выполнения функции.""" @wraps(func) def wrapper(*args, **kwargs): start_time = time.perf_counter() result = func(*args, **kwargs) end_time = time.perf_counter() run_time = end_time - start_time print(f"[Timer] Функция {func.__name__} отработала за {run_time:.4f} сек.") return result return wrapper @benchmark def process_large_data(): time.sleep(1.2) # Эмулируем тяжелую работу return "Готово" process_large_data() # Вывод: [Timer] Функция process_large_data отработала за 1.2014 сек.
2. Продвинутый повторитель (@retry): спасаемся от моргающей сети
В предыдущих главах мы уже писали базовый retry. Давайте сделаем его «взрослым» (production-ready). Мы добавим возможность указывать, какие именно ошибки нужно перехватывать (ведь если упала ValueError — проблема в коде, и повторять бессмысленно, а если ConnectionError — проблема в сети, и стоит подождать).
import time from functools import wraps def retry(times=3, delay=1, exceptions=(Exception,)): """ Повторяет выполнение функции при возникновении определенных исключений. :param times: количество попыток :param delay: пауза между попытками (в секундах) :param exceptions: кортеж с исключениями, которые нужно перехватывать """ def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, times + 1): try: return func(*args, **kwargs) except exceptions as e: print(f"[{func.__name__}] Ошибка {type(e).__name__}: {e}.") if attempt == times: print("Все попытки исчерпаны. Падаем.") raise print(f"Ждем {delay} сек. и пробуем снова (Попытка {attempt + 1}/{times})...") time.sleep(delay) return wrapper return decorator # Перехватываем только ошибки соединения @retry(times=3, delay=2, exceptions=(ConnectionError, TimeoutError)) def fetch_api_data(): # ... тут логика похода в сеть ... raise ConnectionError("Сервер разорвал соединение")
3. Проверка прав (@require_auth): эмуляция middleware
Если вы писали на Flask, Django или FastAPI, вы наверняка использовали декораторы вроде @login_required. Они защищают определенные функции (эндпоинты) от неавторизованных пользователей. Написать такой механизм самому проще простого:
from functools import wraps # Эмулируем контекст текущего пользователя (в реальном приложении # это может быть объект из сессии или JWT-токена) current_user = {"username": "admin", "role": "guest"} def require_auth(allowed_roles): """Проверяет, есть ли у пользователя нужная роль для выполнения функции.""" def decorator(func): @wraps(func) def wrapper(*args, **kwargs): user_role = current_user.get("role") if user_role not in allowed_roles: raise PermissionError( f"Доступ запрещен! У пользователя роль '{user_role}', " f"а требуется одна из: {allowed_roles}" ) return func(*args, **kwargs) return wrapper return decorator # Защищаем важную функцию @require_auth(allowed_roles=["admin", "moderator"]) def delete_database(): print("База данных успешно удалена!") # При текущем current_user = {"role": "guest"} вызов упадет с PermissionError # delete_database()
4. Кэширование (@memoize): не считаем одно и то же дважды
Представьте, что у вас есть функция, которая делает сложный математический расчет или тяжелый запрос к БД. Если вызывать её с одними и теми же аргументами, она каждый раз будет тратить ресурсы заново.
Давайте научим её «запоминать» результаты (этот процесс называется мемоизацией).
from functools import wraps def memoize(func): """Кэширует результаты выполнения функции.""" cache = {} # Здесь будем хранить уже вычисленные результаты @wraps(func) def wrapper(*args): # В качестве ключа используем переданные аргументы if args in cache: print(f"[Cache] Беру результат для {args} из памяти") return cache[args] print(f"[Compute] Вычисляю результат для {args}...") result = func(*args) cache[args] = result return result return wrapper @memoize def fibonacci(n): if n < 2: return n return fibonacci(n-1) + fibonacci(n-2) print(fibonacci(10)) # Вывод: покажет цепочку "Вычисляю...", а затем множество "Беру результат из памяти"
Важное замечание про кэш:
Код выше мы написали исключительно в учебных целях, чтобы показать, как легко декоратор может хранить состояние (словарь cache живет внутри замыкания).
В реальном продакшене вам не нужно писать этот декоратор руками! В стандартной библиотеке Python уже есть невероятно мощный и оптимизированный модуль:
from functools import lru_cache, cache @lru_cache(maxsize=128) # Хранит последние 128 результатов def heavy_computation(x): pass @cache # (Доступно с Python 3.9) Кэширует всё без ограничений по размеру def another_heavy_task(y): pass
9. Заключение. С великой силой приходит великая ответственность
Мы с вами проделали большой путь: от базового понимания, что функции в Python можно передавать как переменные, до создания собственных фабрик декораторов и элегантных классов с кэшированием.
Давайте кратко подытожим, что нужно запомнить:
Декоратор — это не магия. Это просто функция (или вызываемый объект), которая принимает другую функцию и расширяет её поведение.
Пишите универсально. Всегда используйте
*argsи**kwargs, чтобы не сломать передачу аргументов, и не забывайте проreturnвнутри обертки.Сохраняйте лицо. Ваш лучший друг —
@wraps(func)из модуляfunctools. Без него вы потеряете имена функций и документацию, превратив отладку в кошмар.Выбирайте правильный инструмент. Для простых оберток используйте функции. Если нужно хранить состояние или передавать сложную конфигурацию — присмотритесь к классам с методом
__call__.
Главное правило: не злоупотребляйте!
Когда разработчик впервые осознает всю мощь декораторов, у него возникает непреодолимое желание обернуть ими вообще всё. Это классический «синдром золотого молотка».
Остановитесь. Декораторы созданы для сквозного функционала (инфраструктурные вещи, которые не касаются бизнес-логики). Если вы начнете запихивать в декораторы саму бизнес-логику, ваш код станет нечитаемым.
Избегайте «декораторной лазаньи». Посмотрите на этот ужас:
@app.route('/api/data') @require_auth(roles=['admin']) @rate_limit(requests=100, window=60) @retry(times=3) @catch_exceptions @log_execution_time def get_sensitive_data(): return db.fetch_data()
С виду кажется, что код чистый, ведь сама функция занимает всего одну строку. Но под капотом здесь скрыто шесть слоев вложенности!
Порядок выполнения таких декораторов (снизу вверх или сверху вниз?) часто сбивает с толку даже опытных разработчиков.
Если внутри
db.fetch_data()произойдет ошибка, ваш трейсбек (Traceback) будет размером с Войну и мир, пробираясь через 6 разныхwrapper'ов.Неявное поведение — враг поддержки кода. Явное лучше неявного (The Zen of Python).
Анонсы новых статей, полезные материалы, а так же если в процессе у вас возникнут сложности, обсудить их или задать вопрос по этой статье можно в моём Telegram-сообществе. Смело заходите, если что-то пойдет не так, — постараемся разобраться вместе.
Используйте декораторы там, где они экономят время и убирают дублирование, но не превращайте их в инструмент для запутывания коллег.
