Вы написали полезную функцию. Всё работает. Но вдруг понадобилось замерять время её выполнения. Или логировать аргументы. Или проверять права доступа. Переписывать каждую функцию? Нет. В Python есть элегантный механизм — декораторы. Это функции, которые принимают другую функцию и возвращают её улучшенную версию. Представьте, что вы кладёте функцию в «обёртку», которая добавляет нужное поведение до или после вызова.
Сегодня разберём:
- что такое декораторы и как они работают,
- как написать свой первый декоратор,
- декораторы с аргументами,
- как применять несколько декораторов сразу,
- реальные примеры (таймер, логгер, проверка авторизации).
1. Функции — это объекты
В Python функции — полноценные объекты. Их можно присваивать переменным, передавать в другие функции и возвращать из функций.
def say_hello():
print("Привет!")
greet = say_hello # присваиваем
greet() # Привет!
Это свойство — основа декораторов.
2. Первый декоратор: обёртка без магии
Декоратор — это функция, которая принимает функцию и возвращает новую функцию (обычно через def внутри).
def my_decorator(func):
def wrapper():
print("Что-то делаем ДО")
func()
print("Что-то делаем ПОСЛЕ")
return wrapper
def say_hello():
print("Привет!")
say_hello = my_decorator(say_hello)
say_hello()
Вывод:
Что-то делаем ДО
Привет!
Что-то делаем ПОСЛЕ
Синтаксический сахар: вместо say_hello = my_decorator(say_hello) используют символ @:
@my_decorator
def say_hello():
print("Привет!")
say_hello() # результат тот же
@my_decorator — это просто удобная запись.
3. Декоратор для функций с аргументами
Если наша функция принимает аргументы, нужно, чтобы wrapper тоже их принимал и передавал дальше. Используем *args и **kwargs.
def debug(func):
def wrapper(*args, **kwargs):
print(f"Вызов {func.__name__} с аргументами: {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"Результат: {result}")
return result
return wrapper
@debug
def add(a, b):
return a + b
add(3, 5)
Вывод:
Вызов add с аргументами: (3, 5), {}
Результат: 8
4. Декораторы, которые возвращают значение
Важно: wrapper должен возвращать то, что возвращает исходная функция. Иначе вы потеряете результат.
def preserve_result(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result # обязательно
return wrapper
5. Декораторы с параметрами (декораторы высшего порядка)
Иногда нужно передать декоратору аргументы — например, количество повторений или уровень логирования. Тогда создаётся трёхуровневая вложенность:
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_hello():
print("Привет!")
say_hello() # напечатает "Привет!" три раза
Синтаксис: сначала вызывается repeat(3), она возвращает настоящий декоратор decorator, который применяется к say_hello.
6. Сохранение метаданных функции (functools.wraps)
Когда вы оборачиваете функцию, её имя и документация заменяются на имя и документацию wrapper. Это мешает отладке. Исправляется через @wraps из модуля functools.
from functools import wraps
def my_decorator(func):
@wraps(func) # копирует имя, докстринг и т.д.
def wrapper(*args, **kwargs):
"""Обёртка"""
return func(*args, **kwargs)
return wrapper
@my_decorator
def say_hello():
"""Приветственная функция"""
print("Привет!")
print(say_hello.__name__) # say_hello (а не wrapper)
print(say_hello.__doc__) # Приветственная функция
7. Несколько декораторов на одну функцию
Можно навесить несколько декораторов. Они применяются снизу вверх (ближайший к функции выполняется первым).
@decorator_a
@decorator_b
def func():
pass
# Эквивалентно: func = decorator_a(decorator_b(func))
8. Реальные примеры
Таймер для измерения времени
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} заняла {end - start:.6f} сек")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
slow_sum(1000000)
Логирование вызовов в файл
def log_to_file(filename):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
with open(filename, "a", encoding="utf-8") as f:
f.write(f"Вызвана {func.__name__} с {args}, {kwargs}\n")
return func(*args, **kwargs)
return wrapper
return decorator
@log_to_file("log.txt")
def multiply(a, b):
return a * b
Проверка типов аргументов
def type_check(expected_types):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for (arg, expected) in zip(args, expected_types):
if not isinstance(arg, expected):
raise TypeError(f"Аргумент {arg} должен быть {expected}")
return func(*args, **kwargs)
return wrapper
return decorator
@type_check((int, int))
def add(a, b):
return a + b
add(3, "5") # TypeError
9. Типичные ошибки новичков
❌ Забыли return в wrapper
Функция возвращает None вместо ожидаемого результата.
❌ Забыли *args, **kwargs
Декоратор работает только с функциями без аргументов — при попытке передать аргументы получите ошибку.
❌ Не использовали @wraps
Усложняется отладка, теряется имя функции и документация.
❌ Путаница с порядком при нескольких декораторах
Помните: ближайший к функции применяется первым.
Заключение
Декораторы — это элегантный способ расширять поведение функций без изменения их исходного кода. Вы узнали:
- как создавать простые и параметризованные декораторы,
- как сохранять метаданные через functools.wraps,
- как применять несколько декораторов,
- увидели реальные примеры (таймер, логгер, проверка типов).
Теперь ваш код может быть и красивым, и функциональным. Следующая статья будет о тестировании — как убедиться, что ваш код работает правильно, и не бояться его менять. А пока — попробуйте написать декоратор, который перехватывает и обрабатывает исключения, чтобы программа не падала.
Делитесь в комментариях, какой декоратор вы написали первым и где он пригодился.
Статья подготовлена для канала «Код как искусство». Подписывайтесь, чтобы писать код, который обрастает полезными обёртками, а не костылями.