Привет, Хабр! Каждый, кто пишет на Python, видел этот синтаксис: @decorator
. Это не просто "синтаксический сахар", а мощный инструмент для написания чистого и поддерживаемого кода. Если вы до сих пор не писали свои декораторы, то после этой статьи — точно начнете.
1. Введение: решаем реальную проблему
Хватит теории, начнем с боли. Представьте, что вам нужно замерить время выполнения нескольких функций. Скорее всего, ваш код будет выглядеть так:
import time
def fetch_data_from_db():
start_time = time.time()
# --- Основная логика ---
print("Получаем данные...")
time.sleep(2)
print("Данные получены.")
# --- Конец основной логики ---
end_time = time.time()
print(f"Функция выполнилась за {end_time - start_time:.2f} сек.")
def calculate_complex_report():
start_time = time.time()
# --- Основная логика ---
print("Считаем отчет...")
time.sleep(3)
print("Отчет готов.")
# --- Конец основной логики ---
end_time = time.time()
print(f"Функция выполнилась за {end_time - start_time:.2f} сек.")
Этот код работает, но у него две серьезные проблемы:
Нарушение принципа DRY: Мы дублируем код таймера в каждой функции. Хотим изменить логику замера? Придется править везде.
Засорение логики: Замер времени — это служебная задача. Она "размазана" по коду, который должен заниматься получением данных или расчетами.
А теперь представьте, что тот же самый результат можно получить так:
@timer
def fetch_data_from_db():
print("Получаем данные...")
time.sleep(2)
print("Данные получены.")
@timer
def calculate_complex_report():
print("Считаем отчет...")
time.sleep(3)
print("Отчет готов.")
Чисто, лаконично, и основная логика функций не тронута. Служебная функциональность вынесена в @timer
.
Вот это и есть декоратор. По сути, это функция, которая "оборачивает" другую функцию для расширения ее возможностей. В этой статье мы с нуля, шаг за шагом, разберем, как они устроены, и научимся создавать свои — от простых логгеров до более сложных и практичных решений.
2. Основы основ: что нужно знать перед стартом
Прежде чем мы напишем свой первый @decorator
, нужно убедиться, что мы говорим на одном языке. Декораторы — это не какая-то сторонняя магия, а элегантное применение трех фундаментальных концепций Python. Давайте быстро их освежим.
А. Функции — объекты первого класса
В Python всё является объектом, и функции не исключение. Это значит, что с функцией можно обращаться так же, как с любым другим объектом (числом, строкой, списком):
Присваивать переменной:
def say_hello(name): return f"Hello, {name}!" # Присваиваем сам объект функции, а не ее результат greet = say_hello # Теперь greet - это еще одно имя для той же функции print(greet("Habr")) # Вывод: Hello, Habr!
Передавать в качестве аргумента другой функции:
def apply_function(func, value): # Вызываем функцию, которую нам передали return func(value) def double(x): return x * 2 result = apply_function(double, 10) print(result) # Вывод: 20
Возвращать из другой функции:
def get_greeter(greeting): def greeter_func(name): return f"{greeting}, {name}!" # Возвращаем объект функции return greeter_func greet_morning = get_greeter("Good morning") greet_evening = get_greeter("Good evening") print(greet_morning("Alice")) print(greet_evening("Bob")) # Вывод: # Good morning, Alice! # Good evening, Bob!
Эта гибкость — ключ к пониманию декораторов. Мы будем постоянно передавать функции в другие функции и возвращать их.
Б. Вложенные функции
Как вы уже заметили в примере выше, Python позволяет определять функции внутри других функций.
def outer_func():
print("Я внешняя функция.")
def inner_func():
print("А я внутренняя, и меня не видно снаружи.")
# Вызвать внутреннюю функцию можно только внутри внешней
inner_func()
outer_func()
# Вывод:
# Я внешняя функция.
# А я внутренняя, и меня не видно снаружи.
# Эта строка вызовет ошибку NameError, inner_func не определена в глобальной области
# inner_func()
Вложенные функции полезны для инкапсуляции и организации кода, но их настоящая сила раскрывается в сочетании со следующим пунктом.
В. Замыкания (Closures)
Замыкание — это когда вложенная функция "помнит" переменные из той области видимости, в которой она была создана, даже если внешняя функция уже завершила свою работу.
Звучит сложно, но пример всё прояснит:
def make_multiplier(n):
# 'n' - это так называемая "свободная переменная"
def multiplier(x):
# Внутренняя функция "захватывает" и "помнит" значение 'n'
return x * n
return multiplier
# Вызываем внешнюю функцию. Она отрабатывает и возвращает нам
# внутреннюю функцию multiplier, которая "запомнила", что n=3
times3 = make_multiplier(3)
# Вызываем внешнюю функцию еще раз. Она создает НОВЫЙ
# экземпляр multiplier, который "запомнил", что n=5
times5 = make_multiplier(5)
# Теперь вызываем сохраненные внутренние функции
print(times3(10)) # Выведет 30
print(times5(10)) # Выведет 50
В этом примере times3
и times5
— это замыкания. Каждая из них — это функция multiplier
, которая хранит в себе ссылку на свое лексическое окружение, а именно на переменную n
, которая существовала в момент ее создания.
Именно этот механизм — функция, которая принимает другую функцию и возвращает вложенную функцию, "помнящую" исходную — и лежит в самом сердце декораторов.
Теперь, когда мы заложили фундамент, мы готовы построить наш первый декоратор.
3. Наш первый декоратор: шаг за шагом
Итак, мы вооружились знаниями о функциях как объектах и замыканиях. Теперь давайте соберем из этих "кирпичиков" наш первый, по-настоящему работающий декоратор.
Наша цель — создать простой логгер, который будет сообщать о вызове функции.
Шаг 1: "Ручное" декорирование (без @)
Забудем на минуту о символе @
. Как бы мы решили задачу, используя только что изученные концепции? Нам нужна функция, которая примет другую функцию и добавит ей "обертку".
Создадим функцию-декоратор. Назовем ее
simple_logger
. Она будет принимать один аргумент — функцию, которую мы хотим "украсить" (func
).Внутри нее определим "обертку". Назовем ее
wrapper
. Именно эта внутренняя функция будет содержать новую логику: печать сообщения до вызова исходной функции и после него.Внутри обертки вызовем исходную функцию. Мы ведь хотим расширить, а не заменить ее поведение. Важно не забыть вернуть результат ее работы.
Вернем обертку. Наш
simple_logger
должен вернуть объект функцииwrapper
.
Посмотрим на код:
def simple_logger(func):
"""Простой декоратор для логирования вызова функции."""
def wrapper():
print(f"Выполняется функция: {func.__name__}...") # Логика ДО
result = func() # Вызываем исходную функцию
print(f"Функция {func.__name__} завершила выполнение.") # Логика ПОСЛЕ
return result
return wrapper
Теперь применим его к нашей функции вручную. Смотрите, что происходит:
def say_whee():
"""Простая функция, которая возвращает строку."""
print("Whee!")
return "Готово"
# "Декорируем" нашу функцию вручную
# Передаем say_whee в simple_logger, получаем обратно wrapper
# и записываем его в переменную с тем же именем say_whee.
wrapped_say_whee = simple_logger(say_whee)
# Теперь `wrapped_say_whee` — это наша "улучшенная" функция
print("\nВызываем обернутую функцию:")
result = wrapped_say_whee()
print(f"Результат работы: {result}")
Вывод будет таким:
Вызываем обернутую функцию:
Выполняется функция: say_whee...
Whee!
Функция say_whee завершила выполнение.
Результат работы: Готово
Мы фактически подменили нашу исходную функцию say_whee
на wrapper
, который "помнит" про say_whee
благодаря замыканию.
Шаг 2: Магия символа @
Конструкция wrapped_say_whee = simple_logger(say_whee)
выглядит немного громоздко. Программисты любят краткость и читаемость, поэтому в Python для этого придумали специальный синтаксис — тот самый символ @
.
Строка @simple_logger
, размещенная прямо над определением функции, делает в точности то же самое, что мы сделали вручную.
Давайте перепишем наш пример:
# Наш декоратор остается без изменений
def simple_logger(func):
def wrapper():
print(f"Выполняется функция: {func.__name__}...")
result = func()
print(f"Функция {func.__name__} завершила выполнение.")
return result
return wrapper
# А вот применение становится гораздо изящнее
@simple_logger
def say_hello():
"""Еще одна простая функция."""
print("Hello, Habr!")
return "Успех"
# Просто вызываем функцию, как обычно. Python уже "подменил" ее за нас.
print("\nВызываем функцию, украшенную через @:")
output = say_hello()
print(f"Результат работы: {output}")
Вывод будет абсолютно аналогичным:
Вызываем функцию, украшенную через @:
Выполняется функция: say_hello...
Hello, Habr!
Функция say_hello завершила выполнение.
Результат работы: Успех
Поздравляю! Вы только что поняли самый главный принцип работы декораторов.
@my_decorator
над def my_func():
— это просто красивый способ написать my_func = my_decorator(my_func)
.
Теперь, когда мы видим функцию, украшенную декоратором, мы понимаем, что при ее вызове на самом деле будет выполняться код из "обертки" декоратора. Но это еще не все. Что если у нашей функции есть аргументы? Об этом — в следующем разделе.
4. Усложняем задачу: аргументы и метаданные
Наш первый декоратор работает, но он крайне ограничен. Что произойдет, если мы попробуем задекорировать функцию, которая принимает аргументы? Давайте проверим.
@simple_logger
def add(a, b):
"""Эта функция складывает два числа."""
return a + b
# Пытаемся вызвать...
# add(5, 10)
# TypeError: wrapper() takes 0 positional arguments but 2 were given
Мы получаем TypeError
, и это логично. Наша функция-обертка wrapper()
не была рассчитана на прием каких-либо аргументов, в то время как мы пытаемся передать ей 5
и 10
.
Пробрасываем аргументы с *args и **kwargs
Чтобы исправить это, нам нужно сделать две вещи:
Научить
wrapper
принимать любое количество позиционных (*args
) и именованных (**kwargs
) аргументов.Передать эти аргументы "транзитом" в исходную функцию
func
при ее вызове.
Модифицируем наш декоратор:
def logger_with_args(func):
def wrapper(*args, **kwargs): # 1. Принимаем любые аргументы
print(f"Выполняется {func.__name__} с аргументами {args} и {kwargs}...")
result = func(*args, **kwargs) # 2. Передаем их в исходную функцию
print(f"{func.__name__} вернула результат: {result}")
return result
return wrapper
@logger_with_args
def add(a, b):
"""Эта функция складывает два числа."""
return a + b
@logger_with_args
def greet(name, greeting="Привет"):
"""Эта функция формирует приветствие."""
return f"{greeting}, {name}!"
# Теперь все работает!
print(add(5, 10))
print("-" * 20)
print(greet("Мир", greeting="Здравствуй"))
Вывод:
Выполняется add с аргументами (5, 10) и {}...
add вернула результат: 15
15
--------------------
Выполняется greet с аргументами ('Мир',) и {'greeting': 'Здравствуй'}...
greet вернула результат: Здравствуй, Мир!
Здравствуй, Мир!
Отлично! Теперь наш декоратор универсален и может работать с любыми функциями, независимо от их сигнатуры. Но мы столкнулись с другой, более коварной проблемой.
Проблема потерянных метаданных
Давайте попробуем получить информацию о нашей "украшенной" функции add
. В Python это можно сделать с помощью встроенной функции help()
или обратившись напрямую к специальным атрибутам.
print(add.__name__)
# Ожидаем: 'add'
# Получаем: 'wrapper'
print(add.__doc__)
# Ожидаем: 'Эта функция складывает два числа.'
# Получаем: None
Что произошло? Декоратор подменил нашу функцию add
своей внутренней функцией wrapper
. Теперь, с точки зрения Python, add
— это wrapper
. Мы потеряли исходное имя, строку документации (docstring
) и другие метаданные.
Это серьезная проблема. Инструменты для отладки, автоматической генерации документации и просто другие разработчики, которые будут читать ваш код, потеряют важную информацию об исходной функции.
Решение: functools.wraps
К счастью, создатели Python позаботились об этой проблеме. В стандартной библиотеке functools
есть специальный декоратор @wraps
, созданный для... декорирования декораторов!
Он работает как "копировальная машина" для метаданных: @wraps(func)
копирует атрибуты __name__
, __doc__
, __annotations__
и другие из исходной функции func
в функцию-обертку wrapper
.
Давайте напишем финальную, каноническую версию нашего декоратора:
import functools
def professional_logger(func):
@functools.wraps(func) # <--- Вот и решение!
def wrapper(*args, **kwargs):
"""Это внутренняя функция-обертка."""
print(f"Вызов: {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f"Результат {func.__name__}: {result}")
return result
return wrapper
@professional_logger
def multiply(x, y):
"""Умножает два числа и возвращает результат."""
return x * y
# Проверяем метаданные снова
print(f"Имя функции: {multiply.__name__}")
print(f"Docstring: {multiply.__doc__}")
# И вызываем ее
print("\nРезультат вызова:")
multiply(7, 8)
Теперь вывод именно такой, как мы и ожидали:
Имя функции: multiply
Docstring: Умножает два числа и возвращает результат.
Результат вызова:
Вызов: multiply((7, 8), {})
Результат multiply: 56
Запомните правило: всегда используйте @functools.wraps
при написании своих декораторов. Это признак профессионального и качественного кода.
Теперь мы готовы перейти к более сложным сценариям, например, к созданию декораторов, которые сами могут принимать аргументы.
5. Фабрика декораторов: декораторы с параметрами
Мы научились создавать универсальные декораторы, но что если мы хотим сделать их настраиваемыми? Например, вместо простого @timer
, мы хотим @log_level("INFO")
. Или, что еще интереснее, мы хотим декоратор @repeat(num_times=3)
, который выполнит функцию указанное количество раз.
Для этого нам понадобится "фабрика декораторов" — функция, которая создает и возвращает нам нужный декоратор.
Проблема
Давайте вспомним, как работает синтаксис @
:
# Эта строка:
@my_decorator
def my_func(): ...
# ...эквивалентна этой:
my_func = my_decorator(my_func)
Декоратор (my_decorator
) — это функция, которая принимает в качестве единственного аргумента другую функцию (my_func
).
Но когда мы пишем @repeat(3)
, мы сначала вызываем repeat(3)
. Результат этого вызова и должен быть настоящим декоратором, который, в свою очередь, примет нашу функцию.
Звучит как лишний уровень вложенности? Да, именно так и есть.
Решение: дополнительный уровень обертки
Чтобы создать декоратор с параметрами, нам нужна трехуровневая структура:
Фабрика (
decorator_factory
): Внешняя функция. Она принимает параметры для будущего декоратора (например,num_times=3
). Ее единственная задача — вернуть готовую функцию-декоратор.Декоратор (
decorator
): Средний уровень. Это та самая функция, которую вернет фабрика. Она, как и раньше, принимает в качестве аргумента декорируемую функцию (func
).Обертка (
wrapper
): Внутренний уровень. Она, как и раньше, выполняет основную логику, используя и параметры из фабрики, и функцию из декоратора.
Давайте напишем наш @repeat
:
import functools
# Уровень 1: Фабрика. Принимает параметры для декоратора.
def repeat(num_times):
print(f"Фабрика вызвана с num_times={num_times}. Она вернет декоратор.")
# Уровень 2: Декоратор. Принимает декорируемую функцию.
def decorator_repeat(func):
print(f"Декоратор создан. Он обернет функцию {func.__name__}.")
# Уровень 3: Обертка. Выполняет всю работу.
@functools.wraps(func)
def wrapper_repeat(*args, **kwargs):
print(f"Обертка вызвана. Повторяем {num_times} раз...")
last_result = None
for _ in range(num_times):
last_result = func(*args, **kwargs)
return last_result # Вернем результат последнего вызова
return wrapper_repeat # Декоратор возвращает обертку
return decorator_repeat # Фабрика возвращает декоратор
Теперь применим его и посмотрим, что происходит "под капотом":
@repeat(num_times=3)
def greet(name):
"""Простая функция приветствия."""
print(f"Привет, {name}!")
# Вызываем нашу функцию
print("\n--- Начинаем вызов greet('Хабр') ---")
greet("Хабр")
print("--- Вызов завершен ---")
Результат выполнения:
Фабрика вызвана с num_times=3. Она вернет декоратор.
Декоратор создан. Он обернет функцию greet.
--- Начинаем вызов greet('Хабр') ---
Обертка вызвана. Повторяем 3 раз...
Привет, Хабр!
Привет, Хабр!
Привет, Хабр!
--- Вызов завершен ---
Обратите внимание на порядок вывода: фабрика и декоратор вызываются один раз, в момент определения функции greet
. А вот wrapper
вызывается каждый раз, когда мы вызываем greet("Хабр")
.
Давайте по шагам разберем, что сделал Python:
Он увидел
@repeat(num_times=3)
.Он немедленно вызвал
repeat(num_times=3)
. Эта функция (наша фабрика) отработала и вернула нам функциюdecorator_repeat
.Теперь Python видит ситуацию так, как будто мы написали
@decorator_repeat
над нашей функцией.Он применяет этот
decorator_repeat
кgreet
, выполняяgreet = decorator_repeat(greet)
.В результате
greet
теперь ссылается наwrapper_repeat
, который "помнит" и проnum_times=3
(из фабрики), и про исходную функциюgreet
(из декоратора) благодаря замыканиям.
Этот паттерн невероятно мощный. Он позволяет создавать гибкие и переиспользуемые инструменты, например, для проверки прав доступа:
# Концептуальный пример
@require_role('admin')
def delete_user(user_id):
# ... логика удаления
pass
Здесь require_role
— это фабрика, которая создает декоратор, проверяющий, что у текущего пользователя есть роль 'admin'.
Теперь, когда мы освоили и эту технику, давайте посмотрим на несколько классических примеров декораторов, которые встречаются в реальных проектах.
6. Практические примеры из реальной жизни
Теория — это хорошо, но настоящая ценность любого инструмента проявляется в деле. Давайте напишем несколько декораторов, которые вы с легкостью сможете адаптировать для своих проектов.
Пример 1: @timer — измеряем время выполнения
Мы начали статью с этой проблемы, а теперь напишем для нее каноническое решение. Этот декоратор будет измерять, сколько времени заняло выполнение функции, и выводить результат. Для большей точности воспользуемся time.perf_counter()
.
Код декоратора:
import time
import functools
def timer(func):
"""Декоратор, который выводит время выполнения функции."""
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter() # 1. Засекаем время начала
value = func(*args, **kwargs)
end_time = time.perf_counter() # 2. Засекаем время окончания
run_time = end_time - start_time
print(f"Функция {func.__name__!r} выполнилась за {run_time:.4f} с.")
return value
return wrapper_timer
!r
в f-строке вызываетrepr()
для объекта, что добавляет кавычки вокруг имени функции для лучшей читаемости.
Применение:
@timer
def process_data(sleep_time):
"""Функция, имитирующая долгую обработку данных."""
print("Начинаю обработку...")
time.sleep(sleep_time)
print("Обработка завершена.")
return "OK"
process_data(1)
process_data(2)
Результат:
Начинаю обработку...
Обработка завершена.
Функция 'process_data' выполнилась за 1.0007 с.
Начинаю обработку...
Обработка завершена.
Функция 'process_data' выполнилась за 2.0011 с.
Это невероятно удобный инструмент для быстрого профилирования и поиска "узких мест" в вашем коде.
Пример 2: @cache — кэшируем результаты (мемоизация)
Представьте функцию, которая выполняет сложные вычисления. Если вызывать ее несколько раз с одними и теми же аргументами, она будет каждый раз выполнять всю работу заново. Декоратор кэширования может "запомнить" результат для заданного набора аргументов и при повторном вызове мгновенно вернуть его.
Код декоратора:
import functools
def cache(func):
"""Простой декоратор для кэширования результатов функции."""
cache_storage = {}
@functools.wraps(func)
def wrapper_cache(*args, **kwargs):
# Создаем ключ для кэша на основе аргументов.
# kwargs.items() нужно отсортировать, чтобы порядок не влиял на ключ.
cache_key = args + tuple(sorted(kwargs.items()))
if cache_key not in cache_storage:
print(f"Выполняю сложный расчет для {cache_key}...")
cache_storage[cache_key] = func(*args, **kwargs)
else:
print(f"Возвращаю результат из кэша для {cache_key}...")
return cache_storage[cache_key]
return wrapper_cache
Применение:
@cache
def fibonacci(n):
"""Вычисляет n-ное число Фибоначчи (рекурсивно, очень медленно)."""
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# Первый вызов будет очень долгим
print(f"Результат: {fibonacci(10)}")
print("-" * 20)
# Второй вызов будет мгновенным
print(f"Результат: {fibonacci(10)}")
print("-" * 20)
# Даже вызов с меньшим числом будет использовать кэш
print(f"Результат: {fibonacci(5)}")
Результат:
Выполняю сложный расчет для (10,)...
Выполняю сложный расчет для (9,)...
... (много вычислений)
Выполняю сложный расчет для (1,)...
Выполняю сложный расчет для (0,)...
Результат: 55
--------------------
Возвращаю результат из кэша для (10,)...
Результат: 55
--------------------
Возвращаю результат из кэша для (5,)...
Результат: 5
На заметку: В реальных проектах для этой задачи лучше использовать готовое и оптимизированное решение из стандартной библиотеки —
@functools.lru_cache
. Наш пример отлично иллюстрирует сам принцип.
Пример 3: @login_required — проверка прав доступа
Это классический пример из мира веб-разработки (Flask, Django, FastAPI). Декоратор проверяет, авторизован ли пользователь, прежде чем выполнить основную логику функции (например, отобразить страницу профиля).
Код декоратора (концептуальный):
import functools
# --- Представим, что у нас есть такая глобальная сессия ---
# В реальном фреймворке это будет объект запроса.
SESSION = {"username": None, "role": "guest"}
def login_required(func):
"""Декоратор, который проверяет, залогинен ли пользователь."""
@functools.wraps(func)
def wrapper_login_required(*args, **kwargs):
if SESSION.get("username") is None:
# В реальном приложении здесь был бы редирект на страницу логина
# или выброс HTTP-ошибки 401/403.
raise PermissionError("Доступ запрещен: требуется авторизация.")
return func(*args, **kwargs)
return wrapper_login_required
Применение:
@login_required
def view_profile():
"""Показывает страницу профиля пользователя."""
print(f"Добро пожаловать на страницу профиля, {SESSION['username']}!")
# Попытка 1: пользователь не залогинен
try:
view_profile()
except PermissionError as e:
print(e)
# Имитируем успешный логин
SESSION["username"] = "HabrUser"
SESSION["role"] = "user"
# Попытка 2: пользователь залогинен
view_profile()
Результат:
Доступ запрещен: требуется авторизация.
Добро пожаловать на страницу профиля, HabrUser!
Такой подход позволяет полностью отделить логику проверки прав от бизнес-логики обработчиков, делая код чистым и безопасным.
7. Продвинутые темы (краткий обзор)
Мы рассмотрели 95% всего, что вам понадобится в повседневной работе с декораторами. Однако за рамками остались несколько интересных концепций, которые стоит иметь в виду для решения более специфических задач.
А. Декораторы на основе классов
Все это время мы писали декораторы в виде функций. Но их также можно реализовать с помощью классов. Для этого в классе нужно определить два ключевых метода:
__init__()
: Принимает декорируемую функцию и сохраняет ее. Он вызывается в момент декорирования.__call__()
: Делает экземпляр класса "вызываемым", как функцию. Именно этот метод будет выполняться каждый раз, когда вы вызываете задекорированную функцию.
Когда это полезно? Когда вашему декоратору нужно хранить какое-то состояние между вызовами.
Концептуальный пример: декоратор-счетчик
class CallCounter:
def __init__(self, func):
functools.update_wrapper(self, func) # Аналог @wraps для классов
self.func = func
self.num_calls = 0
def __call__(self, *args, **kwargs):
self.num_calls += 1
print(f"Вызов номер {self.num_calls} функции {self.func.__name__!r}")
return self.func(*args, **kwargs)
@CallCounter
def say_hello():
print("Hello!")
say_hello()
say_hello()
# Вывод:
# Вызов номер 1 функции 'say_hello'
# Hello!
# Вызов номер 2 функции 'say_hello'
# Hello!
Обратите внимание, как num_calls
сохраняет свое значение между вызовами. Сделать такое же с помощью функционального декоратора было бы сложнее.
Б. Стекирование (применение нескольких) декораторов
К одной функции можно применить сразу несколько декораторов, просто разместив их друг над другом.
@timer
@professional_logger
def process_heavy_data():
# ... какая-то работа
time.sleep(1)
Важно понимать порядок их применения. Он интуитивно понятен, если представить его как "матрешку" или "луковицу":
При декорировании (один раз): Декор��торы применяются снизу вверх. Сначала
process_heavy_data
оборачивается в@professional_logger
, а затем получившийся результат оборачивается в@timer
.При вызове функции (каждый раз): Код выполняется сверху вниз. Сначала сработает логика
wrapper
'а из@timer
, который затем вызоветwrapper
из@professional_logger
, а тот, в свою очередь, вызовет исходную функцию.
Можно представить это так: timer(professional_logger(process_heavy_data))
.
В. Декорирование классов
Точно так же, как мы декорируем функции, можно декорировать и целые классы. Декоратор класса принимает в качестве аргумента сам класс и может его модифицировать: добавить новые методы, изменить существующие или даже полностью заменить класс на что-то другое.
Концептуальный пример: автоматическое добавление метода
def add_repr(cls):
"""Декоратор, который добавляет классу канонический __repr__."""
def __repr__(self):
params = ', '.join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{cls.__name__}({params})"
cls.__repr__ = __repr__
return cls
@add_repr
class MyData:
def __init__(self, x, y):
self.x = x
self.y = y
data_point = MyData(2, 'a')
print(data_point) # Без декоратора вывод был бы неинформативным
# Вывод: MyData(x=2, y='a')
Это мощная техника для метапрограммирования, часто используемая в фреймворках (например, в dataclasses
или Django ORM) для уменьшения количества шаблонного кода.
Эти темы открывают дверь в мир продвинутого Python, где вы можете изменять поведение кода на лету, создавая гибкие и мощные API.
9. Домашнее задание
Ниже приведены пять задач разного уровня сложности. Попробуйте решить хотя бы одну или две из них, чтобы по-настоящему "почувствовать" декораторы. Не бойтесь экспериментировать и заглядывать в написанный ранее код. Удачи!
Задача 1 (Легкая): Декоратор `@debug`
Условие:
Напишите декоратор debug
, который перед вызовом функции печатает ее имя, а также все позиционные и именованные аргументы, с которыми она вызывается. После вызова он должен напечатать результат, который вернула функция.
Требования:
Декоратор должен быть простым (без параметров).
Он должен корректно работать с любым количеством
*args
и**kwargs
.Используйте
@functools.wraps
для сохранения метаданных.
Пример использования:
@debug
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
greet("World")
# Ожидаемый вывод:
# Вызывается greet('World', greeting='Hello')
# 'greet' вернула 'Hello, World!'
Задача 2 (Легкая+): Фабрика декораторов `@prefix`
Условие:
Напишите фабрику декораторов prefix
, которая принимает строку-префикс. Декоратор, который она возвращает, должен добавлять этот префикс к результату выполнения декорируемой функции.
Требования:
Декорируемая функция должна возвращать строку.
Декоратор должен принимать один аргумент
p
(строку-префикс).
Пример использования:
@prefix(p="[LOG]: ")
def get_status(code):
return f"Status code is {code}"
print(get_status(200)) # Выведет: [LOG]: Status code is 200
print(get_status(404)) # Выведет: [LOG]: Status code is 404
Задача 3 (Средняя): Декоратор `@require_kwargs`
Условие:
Напишите фабрику декораторов require_kwargs
, которая проверяет, что при вызове функции ей были переданы определенные именованные аргументы.
Требования:
Фабрика должна принимать переменное количество строковых аргументов — имена обязательных
kwargs
.Если хотя бы один из требуемых
kwargs
отсутствует при вызове функции, декоратор должен вызыватьTypeError
.Если все требуемые
kwargs
на месте, функция должна выполниться как обычно.
Пример использования:
@require_kwargs("user_id", "role")
def process_request(**kwargs):
print(f"Обработка запроса с параметрами: {kwargs}")
process_request(user_id=10, role="admin", action="delete") # OK
# process_request(user_id=11, action="read") # Вызовет TypeError
Задача 4 (Средняя+): Декоратор `@to_json`
Условие:
Напишите декоратор @to_json
, который преобразует результат выполнения функции (предполагается, что это словарь) в строку формата JSON.
Требования:
Декоратор должен быть простым (без параметров).
Вам понадобится
import json
и функцияjson.dumps()
.Декоратор должен перехватывать результат работы исходной функции, преобразовывать его и возвращать уже строку.
Пример использования:
import json
@to_json
def get_user_data(user_id):
return {"id": user_id, "name": "John Doe", "permissions": ["read", "write"]}
json_string = get_user_data(123)
print(json_string)
# Выведет: {"id": 123, "name": "John Doe", "permissions": ["read", "write"]}
print(type(json_string)) # Выведет: <class 'str'>
Задача 5 (Сложная): Декоратор `@call_limit`
Условие:
Напишите фабрику декораторов call_limit
, которая позволяет вызвать функцию не более указанного количества раз.
Требования:
Фабрика должна принимать один аргумент
limit
— целое число.При попытке вызвать функцию больше, чем
limit
раз, декоратор должен вызыватьValueError
.Каждая задекорированная функция должна иметь свой собственный, независимый счетчик вызовов.
Подсказка: Вам понадобится переменная-счетчик в области видимости декоратора (не фабрики!). Чтобы изменять ее из wrapper
, используйте ключевое слово nonlocal
.
Пример использования:
@call_limit(limit=3)
def send_email(address):
print(f"Отправляю письмо на {address}")
send_email("a@ex.com") # OK
send_email("b@ex.com") # OK
send_email("c@ex.com") # OK
# send_email("d@ex.com") # Вызовет ValueError
Анонс новых статей, полезные материалы, а так же если в процессе решения возникнут сложности, обсудить их или задать вопрос по статье можно в моём Telegram-сообществе.
Уверен, у вас все получится. Вперед, к практике!