Декораторы Python: мощный инструмент для элегантного расширения кода
Для кого эта статья:
- Опытные разработчики, желающие углубить свои знания Python
- Новички, стремящиеся овладеть продвинутыми техниками программирования
Программисты, заинтересованные в улучшении качества и читаемости кода через использование декораторов
Декораторы в Python — один из тех инструментов, который отделяет опытных разработчиков от новичков. Это элегантный механизм, позволяющий расширять функциональность методов и функций без изменения их исходного кода. Владение декораторами поднимает ваш код на новый уровень чистоты и абстракции, превращая сложные и повторяющиеся паттерны в краткие, выразительные конструкции. Если вы готовы перейти от простого использования Python к профессиональному мастерству — декораторы станут вашим секретным оружием. 🚀
Хотите освоить декораторы и другие продвинутые техники Python под руководством экспертов? Обучение Python-разработке от Skypro — это погружение в реальные проекты с первых недель. Наши преподаватели — практикующие разработчики, которые научат вас не только использовать инструменты, но и понимать, когда и почему их применять. Декораторы — лишь верхушка айсберга того, что вы освоите на нашем курсе!
Что такое декораторы в Python и зачем они нужны
Декоратор в Python — это функция, которая принимает другую функцию в качестве аргумента, добавляет к ней новую функциональность и возвращает модифицированную версию без изменения исходного кода. По сути, это паттерн проектирования, позволяющий оборачивать функцию в другую функцию.
Основная идея декораторов базируется на том, что в Python функции являются объектами первого класса, то есть могут передаваться как аргументы, возвращаться из других функций и присваиваться переменным.
Михаил Соколов, Tech Lead Python-разработки
В 2019 году я участвовал в рефакторинге большого проекта для финансового сектора. Кодовая база разрасталась, дублирование логики стало нормой, и отследить использование общих функций было почти невозможно. Когда мы внедрили систему декораторов для логирования, валидации и обработки ошибок, размер кода сократился на 30%, а количество багов при деплое уменьшилось втрое. Вместо написания одинаковых проверок для каждой функции мы просто добавляли декоратор — и код внезапно стал читаемым и поддерживаемым. Декораторы превратили хаос в структуру, не затрагивая основной бизнес-логики.
Ключевые преимущества использования декораторов:
- Разделение ответственности — основной код выполняет свою задачу, а дополнительные функции (логирование, проверки) выносятся в декораторы
- DRY (Don't Repeat Yourself) — избавляемся от дублирования кода
- Улучшение читаемости — функция остаётся чистой, а её расширенный функционал описывается отдельно
- Возможность динамически добавлять поведение — включать и отключать определенное поведение без изменения кода функций
- Удобство тестирования — легче тестировать небольшие функции по отдельности
Примеры задач, где декораторы особенно полезны:
| Задача | Без декораторов | С декораторами |
|---|---|---|
| Логирование | Дублирование кода логов в каждой функции | Единый декоратор @log для всех функций |
| Валидация | Проверки в начале каждой функции | Декоратор @validate_input |
| Кеширование | Сложная логика хранения результатов | Простой @cache декоратор |
| Измерение времени | Дублирование кода замеров | Декоратор @timing |
В синтаксисе Python декоратор обозначается символом @ перед именем функции-декоратора и размещается непосредственно над определением декорируемой функции:
@decorator_function
def target_function():
pass
Это синтаксический сахар для выражения:
def target_function():
pass
target_function = decorator_function(target_function)

Создание простых декораторов: пошаговое руководство
Создадим наш первый декоратор шаг за шагом. Начнем с простейшего варианта — декоратора, который выводит информацию о вызове функции. 🛠️
Шаг 1: Создание функции-обертки
Основа любого декоратора — функция, которая принимает другую функцию и возвращает новую:
def simple_decorator(func):
def wrapper():
print("До выполнения функции")
func()
print("После выполнения функции")
return wrapper
Шаг 2: Применение декоратора
Теперь используем наш декоратор для функции:
@simple_decorator
def say_hello():
print("Привет, мир!")
# Теперь при вызове say_hello() будет выполнен код декоратора
say_hello()
Вывод:
До выполнения функции
Привет, мир!
После выполнения функции
Шаг 3: Передача аргументов в декорируемую функцию
Наш первый декоратор не умеет работать с функциями, принимающими аргументы. Давайте это исправим:
def decorator_with_args(func):
def wrapper(*args, **kwargs):
print(f"Вызов функции {func.__name__} с аргументами {args} и {kwargs}")
result = func(*args, **kwargs)
print(f"Функция {func.__name__} вернула {result}")
return result
return wrapper
@decorator_with_args
def add(a, b):
return a + b
add(5, 3)
Использование args и *kwargs позволяет нашему декоратору работать с любыми аргументами.
Шаг 4: Сохранение метаданных функции
При использовании декораторов теряется исходная информация о функции (имя, документация). Это можно исправить с помощью functools.wraps:
from functools import wraps
def preserving_metadata(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""Это документация обертки"""
return func(*args, **kwargs)
return wrapper
@preserving_metadata
def greet(name):
"""Функция приветствия"""
return f"Привет, {name}!"
print(greet.__name__) # Выведет "greet" вместо "wrapper"
print(greet.__doc__) # Выведет "Функция приветствия"
Шаг 5: Создание декоратора с параметрами
Иногда нам нужны декораторы, принимающие собственные параметры:
def repeat(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_word(word):
print(word)
return word
say_word("Python") # Выведет "Python" 3 раза
Обратите внимание на трехуровневую структуру: внешняя функция принимает параметр декоратора, средняя принимает декорируемую функцию, а внутренняя — аргументы этой функции.
Продвинутые техники создания декораторов в Python
Освоив основы декораторов, перейдем к более сложным техникам, которые откроют новые возможности для улучшения вашего кода. 🔍
Алексей Волков, Python-архитектор
В проекте машинного обучения, над которым я работал, обучение моделей занимало часы. Проблема возникала, когда процесс прерывался — все вычисления приходилось начинать заново. Мы создали декоратор с персистентным кешированием, который сохранял промежуточные результаты на диск. Теперь при повторном запуске уже обработанные данные загружались из кеша. Это сократило время разработки с нескольких дней до часов. Отдельные функции мы декорировали специальными мониторами использования GPU, что позволяло оптимизировать вычисления. Самое удивительное — основной алгоритм оставался чистым и понятным, все служебные функции были элегантно вынесены в декораторы.
Декораторы классов
Декораторы могут применяться не только к функциям, но и к классам:
def add_greeting(cls):
original_init = cls.__init__
def __init__(self, *args, **kwargs):
original_init(self, *args, **kwargs)
self.greeting = "Привет!"
cls.__init__ = __init__
return cls
@add_greeting
class Person:
def __init__(self, name):
self.name = name
person = Person("Иван")
print(person.greeting) # Выведет "Привет!"
Декораторы методов класса
Для методов классов декораторы работают так же, как и для обычных функций, но учитывают первый параметр self:
def method_logger(method):
@wraps(method)
def wrapper(self, *args, **kwargs):
print(f"Вызов метода {method.__name__} экземпляра {self.__class__.__name__}")
return method(self, *args, **kwargs)
return wrapper
class Calculator:
@method_logger
def add(self, a, b):
return a + b
Создание декораторов с состоянием
Иногда декораторам нужно сохранять состояние между вызовами:
def counter(func):
counter.calls = 0
@wraps(func)
def wrapper(*args, **kwargs):
counter.calls += 1
return func(*args, **kwargs)
wrapper.reset_counter = lambda: setattr(counter, 'calls', 0)
wrapper.get_counter = lambda: counter.calls
return wrapper
@counter
def my_func():
pass
my_func()
my_func()
print(my_func.get_counter()) # Выведет 2
my_func.reset_counter()
print(my_func.get_counter()) # Выведет 0
Декораторы на основе классов
Вместо функций можно использовать классы для создания декораторов:
class TimingDecorator:
def __init__(self, func):
self.func = func
wraps(func)(self)
def __call__(self, *args, **kwargs):
import time
start = time.time()
result = self.func(*args, **kwargs)
end = time.time()
print(f"Время выполнения {self.func.__name__}: {end – start:.5f} сек")
return result
@TimingDecorator
def slow_function(delay):
import time
time.sleep(delay)
return "Готово"
Комбинирование нескольких декораторов
Декораторы можно комбинировать, применяя их последовательно:
@decorator1
@decorator2
@decorator3
def function():
pass
Это эквивалентно:
function = decorator1(decorator2(decorator3(function)))
Порядок применения имеет значение — декораторы выполняются снизу вверх.
Сравнение различных подходов к созданию декораторов:
| Подход | Преимущества | Недостатки | Когда использовать |
|---|---|---|---|
| Функциональный декоратор | Простота, понятность | Ограниченное управление состоянием | Для простых задач без состояния |
| Декоратор-класс | Удобное управление состоянием | Более многословный | Когда нужно сохранять состояние между вызовами |
| Декоратор с параметрами | Гибкость настройки | Сложная структура | Когда декоратор нужно настраивать |
| Комбинация декораторов | Модульность | Трудно отлаживать | Для разделения функциональности |
Практические сценарии использования декораторов
Теперь, когда мы разобрались в теории, давайте рассмотрим практические сценарии использования декораторов, которые можно сразу применить в реальных проектах. 💻
1. Логирование и отладка
Один из самых распространенных сценариев — логирование:
import logging
from functools import wraps
def log_execution(func):
@wraps(func)
def wrapper(*args, **kwargs):
logging.info(f"Вызов функции {func.__name__}")
try:
result = func(*args, **kwargs)
logging.info(f"Функция {func.__name__} успешно выполнена")
return result
except Exception as e:
logging.error(f"Ошибка в функции {func.__name__}: {str(e)}")
raise
return wrapper
@log_execution
def divide(a, b):
return a / b
2. Измерение производительности
Отслеживание времени выполнения функций помогает находить узкие места:
import time
from functools import wraps
def timing(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} выполнилась за {end_time – start_time:.4f} сек")
return result
return wrapper
@timing
def process_data(data):
# Обработка данных
time.sleep(0.5) # Имитация работы
return data
3. Кеширование результатов
Для функций, выполняющих тяжелые вычисления с одинаковыми аргументами:
def memoize(func):
cache = {}
@wraps(func)
def wrapper(*args, **kwargs):
# Создаем хеш из аргументов для использования в качестве ключа
key = str(args) + str(sorted(kwargs.items()))
if key not in cache:
cache[key] = func(*args, **kwargs)
return cache[key]
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Без декоратора это было бы очень медленно
print(fibonacci(35)) # Быстрый результат благодаря кешированию
4. Валидация входных данных
Проверка аргументов перед выполнением функции:
def validate_types(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Получаем аннотации типов из сигнатуры функции
sig = inspect.signature(func)
params = sig.parameters
# Проверяем типы позиционных аргументов
for arg, param in zip(args, params.values()):
if param.annotation != inspect.Parameter.empty and not isinstance(arg, param.annotation):
raise TypeError(f"Аргумент {param.name} должен быть типа {param.annotation.__name__}")
# Проверяем возвращаемое значение
result = func(*args, **kwargs)
if sig.return_annotation != inspect.Signature.empty and not isinstance(result, sig.return_annotation):
raise TypeError(f"Возвращаемое значение должно быть типа {sig.return_annotation.__name__}")
return result
return wrapper
@validate_types
def add_numbers(a: int, b: int) -> int:
return a + b
add_numbers(1, 2) # OK
add_numbers("1", 2) # TypeError: Аргумент a должен быть типа int
5. Ограничение доступа и авторизация
Проверка прав доступа к функциям:
def require_auth(role="user"):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Допустим, у нас есть глобальный объект current_user
if not hasattr(current_user, 'role'):
raise PermissionError("Требуется авторизация")
if current_user.role != role and role != "any":
raise PermissionError(f"Требуется роль: {role}")
return func(*args, **kwargs)
return wrapper
return decorator
@require_auth(role="admin")
def delete_user(user_id):
# Удаление пользователя
pass
6. Декоратор для работы с контекстными менеджерами
Превращение функции в контекстный менеджер:
from contextlib import contextmanager
def with_context(func):
@contextmanager
@wraps(func)
def wrapper(*args, **kwargs):
print("Подготовка ресурсов")
try:
yield func(*args, **kwargs)
finally:
print("Освобождение ресурсов")
return wrapper
@with_context
def process_file(filename):
# Обработка файла
return f"Обработка {filename}"
with process_file("data.txt") as result:
print(result)
Какие задачи решают декораторы в реальных проектах:
- Обработка исключений и повторные попытки выполнения при сбоях
- Асинхронная обработка задач
- Инъекция зависимостей
- Управление транзакциями базы данных
- Ограничение скорости API-запросов (rate limiting)
- Трассировка и профилирование кода
- Автоматическое документирование функций
- Ленивая загрузка данных
Распространённые ошибки при работе с декораторами и их решения
При работе с декораторами даже опытные разработчики часто сталкиваются с неочевидными проблемами. Давайте разберём типичные ошибки и способы их исправления. 🔧
1. Потеря метаданных функции
Ошибка: Декораторы меняют name, doc и module оригинальной функции.
def simple_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@simple_decorator
def greet():
"""Функция приветствия"""
return "Привет!"
print(greet.__name__) # Выведет "wrapper" вместо "greet"
print(greet.__doc__) # Выведет None вместо документации
Решение: Используйте functools.wraps:
from functools import wraps
def proper_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@proper_decorator
def greet():
"""Функция приветствия"""
return "Привет!"
print(greet.__name__) # Правильно выведет "greet"
print(greet.__doc__) # Правильно выведет документацию
2. Неправильное время вычисления аргументов декоратора
Ошибка: Аргументы декоратора вычисляются только один раз при определении функции, а не при каждом вызове.
import time
def log_time(message=time.strftime("%H:%M:%S")):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"{message}: вызов {func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
@log_time()
def task_one():
pass
@log_time()
def task_two():
pass
# Даже если вызвать эти функции с интервалом,
# они покажут одинаковое время в сообщении
time.sleep(10)
task_one()
time.sleep(10)
task_two()
Решение: Перенесите вычисления внутрь обёртки:
def log_time(message=None):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
current_time = time.strftime("%H:%M:%S")
current_message = message or current_time
print(f"{current_message}: вызов {func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
3. Проблемы с декораторами, возвращающими значение
Ошибка: Возвращение значения из декоратора, отличного от функции.
def check_positive(func):
@wraps(func)
def wrapper(x):
if x <= 0:
return "Число должно быть положительным"
return func(x)
return wrapper
@check_positive
def calculate_sqrt(x):
import math
return math.sqrt(x)
result = calculate_sqrt(-4) # Вернёт строку вместо числа
result * 2 # TypeError: can't multiply sequence by non-int
Решение: Генерируйте исключения вместо возврата значений других типов:
def check_positive(func):
@wraps(func)
def wrapper(x):
if x <= 0:
raise ValueError("Число должно быть положительным")
return func(x)
return wrapper
4. Неправильная работа с аргументами декоратора
Ошибка: Путаница в структуре декоратора с параметрами.
# Неправильно
@retry(3) # 3 передаётся в функцию, а не в декоратор
def unstable_function():
pass
Решение: Правильная структура трёхуровневого декоратора:
def retry(attempts):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if i == attempts – 1:
raise
print(f"Попытка {i+1} не удалась, повторяю...")
return wrapper
return decorator
@retry(3) # Теперь корректно
def unstable_function():
import random
if random.random() < 0.7:
raise ValueError("Случайный сбой!")
return "Успех!"
5. Проблемы при комбинировании декораторов
Ошибка: Неучтенный порядок выполнения при использовании нескольких декораторов.
@timing
@log_execution
def process_data():
# Обработка...
pass
Решение: Помните, что декораторы применяются снизу вверх. Если нужно сначала логировать, а потом замерять время:
@log_execution # Выполнится вторым
@timing # Выполнится первым
def process_data():
# Обработка...
pass
Типичные ошибки и решения:
| Проблема | Симптомы | Решение |
|---|---|---|
| Потеря метаданных функции | Неправильные имена и документация в логах или отладке | Использовать @functools.wraps |
| Раннее вычисление аргументов | Неактуальные данные при выполнении | Перенести вычисления в обёртку |
| Несогласованные типы возврата | TypeError при дальнейшей работе с результатом | Генерировать исключения вместо возврата разных типов |
| Неверная структура декоратора | Ошибки при вызове, непредсказуемое поведение | Использовать правильную многоуровневую структуру |
| Путаница в порядке декораторов | Неожиданное поведение при комбинировании | Учитывать, что применение идёт снизу вверх |
Продвинутые рекомендации для работы с декораторами:
- Создавайте небольшие декораторы с единственной ответственностью и комбинируйте их
- Пишите тесты специально для проверки поведения декораторов
- Используйте типизацию для декораторов в аннотациях Python 3.9+
- Для сложных декораторов рассмотрите возможность использования классов вместо функций
- Документируйте поведение декораторов, особенно если они изменяют функциональность декорируемых функций
Декораторы — это не просто синтаксический сахар, а мощный инструмент проектирования, позволяющий улучшить читаемость, переиспользуемость и тестируемость кода. Освоив принципы их создания, вы сможете элегантно решать сложные задачи, избегая дублирования и сохраняя чистоту вашего кода. Помните: хороший декоратор не просто работает — он делает весь проект лучше.