Декораторы Python: мощный инструмент для элегантного расширения кода

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Опытные разработчики, желающие углубить свои знания 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 декоратор обозначается символом @ перед именем функции-декоратора и размещается непосредственно над определением декорируемой функции:

Python
Скопировать код
@decorator_function
def target_function():
pass

Это синтаксический сахар для выражения:

Python
Скопировать код
def target_function():
pass
target_function = decorator_function(target_function)

Пошаговый план для смены профессии

Создание простых декораторов: пошаговое руководство

Создадим наш первый декоратор шаг за шагом. Начнем с простейшего варианта — декоратора, который выводит информацию о вызове функции. 🛠️

Шаг 1: Создание функции-обертки

Основа любого декоратора — функция, которая принимает другую функцию и возвращает новую:

Python
Скопировать код
def simple_decorator(func):
def wrapper():
print("До выполнения функции")
func()
print("После выполнения функции")
return wrapper

Шаг 2: Применение декоратора

Теперь используем наш декоратор для функции:

Python
Скопировать код
@simple_decorator
def say_hello():
print("Привет, мир!")

# Теперь при вызове say_hello() будет выполнен код декоратора
say_hello()

Вывод:

plaintext
Скопировать код
До выполнения функции
Привет, мир!
После выполнения функции

Шаг 3: Передача аргументов в декорируемую функцию

Наш первый декоратор не умеет работать с функциями, принимающими аргументы. Давайте это исправим:

Python
Скопировать код
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:

Python
Скопировать код
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: Создание декоратора с параметрами

Иногда нам нужны декораторы, принимающие собственные параметры:

Python
Скопировать код
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, что позволяло оптимизировать вычисления. Самое удивительное — основной алгоритм оставался чистым и понятным, все служебные функции были элегантно вынесены в декораторы.

Декораторы классов

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

Python
Скопировать код
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:

Python
Скопировать код
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

Создание декораторов с состоянием

Иногда декораторам нужно сохранять состояние между вызовами:

Python
Скопировать код
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

Декораторы на основе классов

Вместо функций можно использовать классы для создания декораторов:

Python
Скопировать код
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 "Готово"

Комбинирование нескольких декораторов

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

Python
Скопировать код
@decorator1
@decorator2
@decorator3
def function():
pass

Это эквивалентно:

Python
Скопировать код
function = decorator1(decorator2(decorator3(function)))

Порядок применения имеет значение — декораторы выполняются снизу вверх.

Сравнение различных подходов к созданию декораторов:

Подход Преимущества Недостатки Когда использовать
Функциональный декоратор Простота, понятность Ограниченное управление состоянием Для простых задач без состояния
Декоратор-класс Удобное управление состоянием Более многословный Когда нужно сохранять состояние между вызовами
Декоратор с параметрами Гибкость настройки Сложная структура Когда декоратор нужно настраивать
Комбинация декораторов Модульность Трудно отлаживать Для разделения функциональности

Практические сценарии использования декораторов

Теперь, когда мы разобрались в теории, давайте рассмотрим практические сценарии использования декораторов, которые можно сразу применить в реальных проектах. 💻

1. Логирование и отладка

Один из самых распространенных сценариев — логирование:

Python
Скопировать код
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. Измерение производительности

Отслеживание времени выполнения функций помогает находить узкие места:

Python
Скопировать код
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. Кеширование результатов

Для функций, выполняющих тяжелые вычисления с одинаковыми аргументами:

Python
Скопировать код
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. Валидация входных данных

Проверка аргументов перед выполнением функции:

Python
Скопировать код
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. Ограничение доступа и авторизация

Проверка прав доступа к функциям:

Python
Скопировать код
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. Декоратор для работы с контекстными менеджерами

Превращение функции в контекстный менеджер:

Python
Скопировать код
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 оригинальной функции.

Python
Скопировать код
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:

Python
Скопировать код
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. Неправильное время вычисления аргументов декоратора

Ошибка: Аргументы декоратора вычисляются только один раз при определении функции, а не при каждом вызове.

Python
Скопировать код
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()

Решение: Перенесите вычисления внутрь обёртки:

Python
Скопировать код
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. Проблемы с декораторами, возвращающими значение

Ошибка: Возвращение значения из декоратора, отличного от функции.

Python
Скопировать код
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

Решение: Генерируйте исключения вместо возврата значений других типов:

Python
Скопировать код
def check_positive(func):
@wraps(func)
def wrapper(x):
if x <= 0:
raise ValueError("Число должно быть положительным")
return func(x)
return wrapper

4. Неправильная работа с аргументами декоратора

Ошибка: Путаница в структуре декоратора с параметрами.

Python
Скопировать код
# Неправильно
@retry(3) # 3 передаётся в функцию, а не в декоратор
def unstable_function():
pass

Решение: Правильная структура трёхуровневого декоратора:

Python
Скопировать код
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. Проблемы при комбинировании декораторов

Ошибка: Неучтенный порядок выполнения при использовании нескольких декораторов.

Python
Скопировать код
@timing
@log_execution
def process_data():
# Обработка...
pass

Решение: Помните, что декораторы применяются снизу вверх. Если нужно сначала логировать, а потом замерять время:

Python
Скопировать код
@log_execution # Выполнится вторым
@timing # Выполнится первым
def process_data():
# Обработка...
pass

Типичные ошибки и решения:

Проблема Симптомы Решение
Потеря метаданных функции Неправильные имена и документация в логах или отладке Использовать @functools.wraps
Раннее вычисление аргументов Неактуальные данные при выполнении Перенести вычисления в обёртку
Несогласованные типы возврата TypeError при дальнейшей работе с результатом Генерировать исключения вместо возврата разных типов
Неверная структура декоратора Ошибки при вызове, непредсказуемое поведение Использовать правильную многоуровневую структуру
Путаница в порядке декораторов Неожиданное поведение при комбинировании Учитывать, что применение идёт снизу вверх

Продвинутые рекомендации для работы с декораторами:

  • Создавайте небольшие декораторы с единственной ответственностью и комбинируйте их
  • Пишите тесты специально для проверки поведения декораторов
  • Используйте типизацию для декораторов в аннотациях Python 3.9+
  • Для сложных декораторов рассмотрите возможность использования классов вместо функций
  • Документируйте поведение декораторов, особенно если они изменяют функциональность декорируемых функций

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

Загрузка...