Декораторы с параметрами в Python: гибкая настройка функций кода

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

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

  • опытные разработчики Python, желающие углубить свои знания
  • студенты и участники курсов по программированию на Python
  • программисты, интересующиеся метапрограммированием и оптимизацией кода

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

Хотите писать код, который будет впечатлять даже опытных Python-разработчиков? На курсе Обучение Python-разработке от Skypro вы научитесь не только создавать декораторы с параметрами, но и применять продвинутые паттерны программирования. Наши студенты пишут код, который читается как поэзия и выполняется как швейцарские часы. От базовых концепций до архитектуры высоконагруженных систем — всё в одном курсе с практическими проектами и поддержкой опытных менторов.

Синтаксис и структура параметризованных декораторов Python

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

Базовая структура декоратора с параметрами выглядит так:

Python
Скопировать код
def decorator_with_args(decorator_arg1, decorator_arg2):
def decorator(func):
def wrapper(*args, **kwargs):
# Здесь используются decorator_arg1 и decorator_arg2
print(f"Декоратор получил аргументы: {decorator_arg1}, {decorator_arg2}")
return func(*args, **kwargs)
return wrapper
return decorator

@decorator_with_args("hello", 42)
def my_function(function_arg):
print(f"Вызвана функция с аргументом {function_arg}")

Обратите внимание на три уровня вложенности:

  1. decoratorwithargs — принимает параметры декоратора
  2. decorator — принимает функцию для декорирования
  3. wrapper — принимает аргументы вызова декорированной функции

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

Уровень функции Принимает Возвращает
decoratorwithargs Параметры декоратора decorator
decorator Декорируемую функцию wrapper
wrapper Аргументы декорируемой функции Результат декорируемой функции

При использовании декораторов с параметрами важно понимать порядок выполнения. Когда интерпретатор встречает строку @decorator_with_args("hello", 42), он сначала выполняет decorator_with_args("hello", 42), что возвращает декоратор, который затем применяется к функции.

Иван Соколов, Python-архитектор

Долгое время я мучился с дублированием кода в проекте аналитической платформы. У нас было множество функций, которые требовали разного уровня логирования в зависимости от контекста. Я писал практически идентичные блоки try-except и логеры для каждой функции.

Решение пришло, когда я внедрил декоратор с параметрами:

Python
Скопировать код
def log_with_level(level='INFO'):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
logger = logging.getLogger(func.__name__)
try:
result = func(*args, **kwargs)
logger.log(getattr(logging, level), f"Успешно выполнено с результатом: {result}")
return result
except Exception as e:
logger.log(getattr(logging, level), f"Ошибка: {e}")
raise
return wrapper
return decorator

После этого код стал удивительно чистым: просто добавляем @log_with_level('DEBUG') или @log_with_level('ERROR') перед функцией — и всё работает. Размер кодовой базы уменьшился на 15%, а читаемость выросла в разы.

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

Реализация декораторов с параметрами: пошаговая техника

Создание декораторов с параметрами требует четкого понимания замыканий и функционального программирования. Давайте разберем пошаговый процесс создания таких декораторов. 🛠️

  1. Определите внешнюю функцию, которая будет принимать параметры декоратора
  2. Внутри неё создайте промежуточную функцию, принимающую декорируемую функцию
  3. В промежуточной функции определите wrapper, который будет оборачивать вызов оригинальной функции
  4. Используйте @wraps из модуля functools для сохранения метаданных оригинальной функции
  5. Верните wrapper из промежуточной функции
  6. Верните промежуточную функцию из внешней

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

Python
Скопировать код
from functools import wraps
import time

def repeat(times=3, delay=1):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
time.sleep(delay)
return result
return wrapper
return decorator

@repeat(times=5, delay=0.5)
def greet(name):
"""Приветствует пользователя по имени"""
print(f"Привет, {name}!")
return name

# Использование
result = greet("Алиса") # Выведет "Привет, Алиса!" 5 раз с интервалом 0.5 сек
print(greet.__name__) # Выведет "greet", а не "wrapper" благодаря @wraps
print(greet.__doc__) # Выведет оригинальную документацию

Обратите внимание, что использование @wraps(func) сохраняет метаданные оригинальной функции, такие как имя функции, докстринг и сигнатуру. Без этого декорированная функция потеряет важную метаинформацию, что может затруднить отладку и документирование.

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

Python
Скопировать код
@repeat() # Будет повторять 3 раза с задержкой 1 секунда
def say_hello():
print("Hello!")

@repeat # Ошибка! Декоратор с параметрами требует вызова
def wrong_usage():
pass

Стандартные ошибки Решение
Неправильный порядок вложенности функций Следуйте шаблону: внешняя функция → декоратор → wrapper
Потеря метаданных функции Используйте @wraps из functools
Проблемы с областью видимости переменных Четко определяйте, какие переменные доступны в каждом замыкании
Забытые скобки при использовании Помните, что декоратор с параметрами всегда требует скобок, даже если параметры не передаются

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

Декораторы с параметрами находят применение во множестве сценариев. Рассмотрим наиболее полезные и распространенные случаи использования, которые помогут оптимизировать ваш код. 🚀

  1. Валидация входных данных — проверка аргументов функции с настраиваемыми правилами
  2. Контроль доступа — ограничение доступа к функциям на основе ролей или прав пользователя
  3. Кэширование с настройкой TTL — хранение результатов функции в кэше на указанное время
  4. Повторение при ошибках — автоматические повторные попытки с настраиваемыми параметрами
  5. Измерение производительности — замер времени выполнения функции с настраиваемыми метриками
  6. Асинхронная обработка — запуск функций в отдельных потоках или процессах

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

Python
Скопировать код
def validate_types(**expected_types):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Получаем имена параметров функции
func_args = func.__code__.co_varnames[:func.__code__.co_argcount]

# Проверяем типы позиционных аргументов
for arg_name, arg_value in zip(func_args, args):
if arg_name in expected_types and not isinstance(arg_value, expected_types[arg_name]):
raise TypeError(f"Аргумент {arg_name} должен быть типа {expected_types[arg_name].__name__}, "
f"получен {type(arg_value).__name__}")

# Проверяем типы именованных аргументов
for kwarg_name, kwarg_value in kwargs.items():
if kwarg_name in expected_types and not isinstance(kwarg_value, expected_types[kwarg_name]):
raise TypeError(f"Аргумент {kwarg_name} должен быть типа {expected_types[kwarg_name].__name__}, "
f"получен {type(kwarg_value).__name__}")

return func(*args, **kwargs)
return wrapper
return decorator

@validate_types(name=str, age=int)
def register_user(name, age):
print(f"Регистрация пользователя: {name}, возраст: {age}")

# Корректное использование
register_user("Алиса", 30) # Регистрация пользователя: Алиса, возраст: 30

# Ошибка
try:
register_user("Боб", "25") # Вызовет TypeError
except TypeError as e:
print(e) # Аргумент age должен быть типа int, получен str

Другой полезный случай — ограничение скорости вызовов API с помощью декоратора с параметрами:

Python
Скопировать код
import time
from functools import wraps

def rate_limit(calls_limit=10, period=60):
"""Ограничивает количество вызовов функции до calls_limit за period секунд"""
calls_history = []

def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
current_time = time.time()

# Удаляем устаревшие записи
while calls_history and calls_history[0] < current_time – period:
calls_history.pop(0)

# Проверяем лимит
if len(calls_history) >= calls_limit:
wait_time = calls_history[0] + period – current_time
raise Exception(f"Превышен лимит вызовов. Повторите через {wait_time:.2f} секунд.")

# Добавляем текущий вызов
calls_history.append(current_time)

return func(*args, **kwargs)
return wrapper
return decorator

@rate_limit(calls_limit=3, period=10)
def request_api():
print("API запрос выполнен")

Анна Петрова, DevOps-инженер

На одном из проектов мы столкнулись с непредсказуемым поведением микросервисов при доступе к внешнему API. Сервисы периодически "падали" из-за таймаутов, но локализовать проблему было сложно.

Я решила добавить декоратор с параметрами для всех функций, взаимодействующих с API:

Python
Скопировать код
def api_call_tracker(service_name, timeout=5):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
try:
return func(*args, **kwargs)
except Exception as e:
elapsed = time.time() – start_time
metrics.increment(f"{service_name}_api_errors", 
tags={"exception": type(e).__name__})
if elapsed >= timeout:
metrics.increment(f"{service_name}_api_timeouts")
raise
finally:
metrics.timing(f"{service_name}_api_latency", 
time.time() – start_time)
return wrapper
return decorator

Применив этот декоратор к каждой функции с указанием имени сервиса, мы быстро выявили, что один из поставщиков API регулярно имел задержки более 10 секунд в определенные часы. Мы добавили кэширование для этого конкретного случая, и проблема была решена. Без декоратора с параметрами собрать такую детальную статистику было бы гораздо сложнее.

Отладка и типичные ошибки при работе с параметрами

Работа с декораторами, принимающими параметры, может быть источником сложно отлаживаемых ошибок. Рассмотрим распространенные проблемы и способы их решения. 🔍

  1. Проблема с областью видимости — параметры декоратора могут "потеряться" из-за неправильного управления замыканиями
  2. Ошибки при вызове декораторов — забытые или неправильно расставленные скобки
  3. Проблемы с сигнатурой функции — потеря метаданных оригинальной функции
  4. Взаимодействие нескольких декораторов — ошибки при использовании цепочки декораторов
  5. Мутация переменных в замыканиях — неожиданное поведение при изменении мутабельных параметров

Проблема мутабельных значений по умолчанию заслуживает особого внимания. Рассмотрим следующий пример:

Python
Скопировать код
def cache_with_timeout(timeout=60, cache={}): # Опасно! Мутабельное значение по умолчанию
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
current_time = time.time()

if key in cache and current_time – cache[key]['time'] < timeout:
return cache[key]['value']

result = func(*args, **kwargs)
cache[key] = {'value': result, 'time': current_time}
return result
return wrapper
return decorator

Проблема здесь в том, что словарь cache создается один раз при определении функции, а не при каждом вызове декоратора. Это приведет к тому, что все функции, использующие этот декоратор, будут делить один и тот же кэш!

Правильная реализация:

Python
Скопировать код
def cache_with_timeout(timeout=60, cache=None):
if cache is None:
cache = {} # Создаем новый словарь для каждого вызова декоратора

def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
current_time = time.time()

if key in cache and current_time – cache[key]['time'] < timeout:
return cache[key]['value']

result = func(*args, **kwargs)
cache[key] = {'value': result, 'time': current_time}
return result
return wrapper
return decorator

При отладке декораторов с параметрами полезно использовать следующие приемы:

  • Добавляйте логирование на каждом уровне вложенности
  • Проверяйте сохранение метаданных функции с помощью print(func.__name__, func.__doc__)
  • Используйте отладчик pdb для пошагового выполнения
  • Внедряйте тесты, явно проверяющие поведение декораторов с различными параметрами
Проблема Признаки Решение
Мутабельные значения по умолчанию Неожиданные взаимные влияния между разными вызовами функций Инициализировать None, создавать новый объект внутри функции
Потеря метаданных Неправильное отображение в документации, проблемы с интроспекцией Использовать @wraps из functools
Перепутанная вложенность Непредсказуемое поведение декоратора Следовать стандартному шаблону с тремя уровнями вложенности
Множественные декораторы Сложности при чтении и отладке цепочек декораторов Применять декораторы в порядке от внешнего к внутреннему

Продвинутые техники создания декораторов с параметрами

Для тех, кто уже освоил основы, существуют более сложные, но мощные техники работы с декораторами, принимающими параметры. Эти подходы расширяют возможности декораторов и делают код более элегантным. 🧠

Одна из продвинутых техник — создание декораторов с опциональными параметрами, которые можно использовать как с параметрами, так и без них:

Python
Скопировать код
def flexible_decorator(func=None, *, param1=None, param2=None):
def actual_decorator(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"Параметры декоратора: {param1}, {param2}")
return function(*args, **kwargs)
return wrapper

# Проверяем, был ли декоратор вызван с аргументами или без
if func is None:
# Вызов с аргументами: @flexible_decorator(param1=value)
return actual_decorator
else:
# Вызов без аргументов: @flexible_decorator
return actual_decorator(func)

# Можно использовать так
@flexible_decorator
def function1():
pass

# Или так
@flexible_decorator(param1="hello", param2=42)
def function2():
pass

Другая интересная техника — создание декораторов классов с параметрами:

Python
Скопировать код
def register_type(type_name, namespace="default"):
def decorator(cls):
# Сохраняем оригинальное имя класса
original_name = cls.__name__

# Регистрируем класс в глобальном реестре
if namespace not in TYPE_REGISTRY:
TYPE_REGISTRY[namespace] = {}

TYPE_REGISTRY[namespace][type_name] = cls

# Можем также модифицировать класс
cls.type_name = type_name
cls.namespace = namespace

return cls
return decorator

@register_type("user", namespace="auth")
class User:
def __init__(self, name):
self.name = name

Также можно комбинировать декораторы с параметрами с другими паттернами проектирования. Например, использовать их вместе с шаблоном "Стратегия":

Python
Скопировать код
def validation_strategy(strategy_name, **strategy_options):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Выбираем стратегию валидации
if strategy_name == "length":
min_length = strategy_options.get('min', 0)
max_length = strategy_options.get('max', float('inf'))

for arg in args:
if isinstance(arg, str) and (len(arg) < min_length or len(arg) > max_length):
raise ValueError(f"Длина строки должна быть между {min_length} и {max_length}")

elif strategy_name == "range":
min_val = strategy_options.get('min', float('-inf'))
max_val = strategy_options.get('max', float('inf'))

for arg in args:
if isinstance(arg, (int, float)) and (arg < min_val or arg > max_val):
raise ValueError(f"Значение должно быть между {min_val} и {max_val}")

# Добавьте другие стратегии по необходимости

return func(*args, **kwargs)
return wrapper
return decorator

@validation_strategy("length", min=3, max=50)
def create_username(username):
return f"Создан пользователь: {username}"

@validation_strategy("range", min=18, max=120)
def set_age(age):
return f"Установлен возраст: {age}"

Для самых продвинутых случаев можно создавать декораторы, которые адаптируются в зависимости от типа декорируемого объекта:

Python
Скопировать код
def smart_decorator(*dec_args, **dec_kwargs):
def decorator(obj):
if isinstance(obj, type):
# Декорируем класс
return _decorate_class(obj, *dec_args, **dec_kwargs)
elif callable(obj):
# Декорируем функцию
return _decorate_function(obj, *dec_args, **dec_kwargs)
else:
raise TypeError(f"Объект {obj} не является ни классом, ни функцией")

return decorator

def _decorate_class(cls, *args, **kwargs):
# Логика для декорирования класса
print(f"Декорирование класса {cls.__name__} с параметрами {args}, {kwargs}")
return cls

def _decorate_function(func, *args, **kwargs):
# Логика для декорирования функции
@wraps(func)
def wrapper(*fn_args, **fn_kwargs):
print(f"Вызов функции {func.__name__} с параметрами декоратора {args}, {kwargs}")
return func(*fn_args, **fn_kwargs)
return wrapper

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

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

Загрузка...