38. Декораторы: добавление поведения к функциям
Декораторы (decorators) — одна из самых мощных возможностей Python для написания чистого, переиспользуемого кода. Они позволяют изменять или улучшать поведение функций (functions), не меняя их фактический код. В этой главе мы продолжим тему функций как объектов первого класса (first-class functions) и замыканий (closures) из главы 23 и разберёмся, как работают декораторы и как эффективно их использовать.
38.1) Что такое декораторы и почему они полезны
Декоратор (decorator) — это функция (function), которая принимает другую функцию в качестве аргумента и возвращает модифицированную версию этой функции. Это возможно потому, что, как мы узнали в главе 23, функции в Python — это объекты первого класса: их можно передавать как аргументы и возвращать из других функций. Декораторы позволяют «оборачивать» дополнительное поведение вокруг существующих функций, упрощая добавление общей функциональности, такой как логирование, замер времени, валидация или контроль доступа, не захламляя вашу основную логику.
Почему декораторы важны
Представьте, что у вас есть несколько функций в программе, и вы хотите логировать, когда вызывается каждая из них. Без декораторов вы могли бы написать что-то вроде этого:
# Без декораторов — дублирование кода логирования
def calculate_total(prices):
print("Calling calculate_total")
result = sum(prices)
print(f"calculate_total returned: {result}")
return result
def find_average(numbers):
print("Calling find_average")
result = sum(numbers) / len(numbers)
print(f"find_average returned: {result}")
return result
def process_order(order_id):
print("Calling process_order")
result = f"Order {order_id} processed"
print(f"process_order returned: {result}")
return result
# Использование функций
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60У такого подхода есть несколько проблем:
- Дублирование кода: строки логирования повторяются в каждой функции
- Смешение ответственности: код логирования смешан с бизнес-логикой
- Сложно поддерживать: если вы хотите изменить формат логирования, вам нужно обновить каждую функцию
- Легко забыть: новые функции могут не включать логирование
Декораторы решают эти проблемы, позволяя отделить поведение логирования от ваших основных функций:
# С декораторами — чисто и удобно поддерживать
# (В этой главе мы узнаем, как создать @log_calls)
@log_calls
def calculate_total(prices):
return sum(prices)
@log_calls
def find_average(numbers):
return sum(numbers) / len(numbers)
@log_calls
def process_order(order_id):
return f"Order {order_id} processed"
# Использование функций даёт тот же вывод
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60В чём разница? Поведение логирования определяется один раз в декораторе @log_calls и переиспользуется везде. Ваши основные функции остаются чистыми и сосредоточенными на своей основной цели.
Распространённые сценарии использования декораторов
Декораторы особенно полезны для:
- Логирование: запись того, когда функции вызываются и что они возвращают
- Замер времени: измерение того, сколько времени выполняются функции
- Валидация: проверка того, что аргументы функции соответствуют определённым требованиям
- Кэширование: сохранение результатов дорогих вызовов функций для повторного использования
- Контроль доступа: проверка прав перед разрешением выполнения функции
- Логика повторов: автоматический повтор неудачных операций
- Проверка типов: валидация типов аргументов и возвращаемых значений
Ключевое преимущество в том, что вы пишете декоратор один раз и можете применить его ко многим функциям одной строкой кода.
38.2) Функции как объекты: основа декораторов
Прежде чем мы сможем понять декораторы (decorators), нам нужно повторить и расширить концепцию того, что функции — это объекты первого класса в Python. Как мы узнали в главе 23, это означает, что функции можно присваивать переменным, передавать как аргументы и возвращать из других функций.
Функции можно присваивать переменным
Когда вы определяете функцию, Python создаёт объект функции и привязывает его к имени:
def greet(name):
return f"Hello, {name}!"
# Объект функции можно присвоить другой переменной
say_hello = greet
# Оба имени ссылаются на один и тот же объект функции
print(greet("Alice")) # Output: Hello, Alice!
print(say_hello("Bob")) # Output: Hello, Bob!Имена greet и say_hello оба ссылаются на один и тот же объект функции. Это фундаментально для того, как работают декораторы.
Функции можно передавать как аргументы
Вы можете передавать функции в другие функции так же, как и любое другое значение:
def apply_twice(func, value):
"""Применить функцию к значению дважды."""
result = func(value)
result = func(result)
return result
def add_five(x):
return x + 5
result = apply_twice(add_five, 10)
print(result) # Output: 20 (10 + 5 = 15, then 15 + 5 = 20)Здесь apply_twice получает функцию add_five как аргумент и вызывает её дважды.
Функции могут возвращать другие функции
Функция может создать и вернуть новую функцию:
def make_multiplier(factor):
"""Создать функцию, которая умножает на заданный коэффициент."""
def multiply(x):
return x * factor
return multiply
times_three = make_multiplier(3)
times_five = make_multiplier(5)
print(times_three(10)) # Output: 30
print(times_five(10)) # Output: 50Функция make_multiplier возвращает новую функцию, которая «помнит» значение factor через замыкание (closure) (как мы узнали в главе 23).
Обёртывание функций: базовый шаблон декоратора
Шаблон декоратора объединяет эти концепции: функция принимает функцию на вход, создаёт функцию-обёртку(wrapper), которая добавляет поведение, и возвращает обёртку:
def simple_wrapper(original_func):
"""Обернуть функцию дополнительным поведением."""
def wrapper():
print("Before calling the function")
result = original_func()
print("After calling the function")
return result
return wrapper
def say_hello():
print("Hello!")
return "greeting"
# Вручную оборачиваем функцию
wrapped_hello = simple_wrapper(say_hello)
return_value = wrapped_hello()
# Output:
# Before calling the function
# Hello!
# After calling the function
print(f"Returned: {return_value}")
# Output: Returned: greetingДавайте проследим, что происходит:
simple_wrapperполучаетsay_helloкакoriginal_func- Она создаёт новую функцию
wrapper, которая:- Печатает "Before calling the function"
- Вызывает
original_func()(то естьsay_hello) - Печатает "After calling the function"
- Возвращает результат
simple_wrapperвозвращает функциюwrapper- Когда мы вызываем
wrapped_hello(), мы на самом деле вызываемwrapper, которая внутри вызывает исходнуюsay_hello
Это базовый шаблон, лежащий в основе всех декораторов.
Обработка функций с аргументами
Обёртка выше работает только с функциями, которые не принимают аргументы. Чтобы она работала с любой функцией, нам нужны *args и **kwargs:
def flexible_wrapper(original_func):
"""Обернуть функцию, которая может принимать любые аргументы."""
def wrapper(*args, **kwargs):
# *args собирает позиционные аргументы
# **kwargs собирает именованные аргументы
print("Before calling the function")
result = original_func(*args, **kwargs)
print("After calling the function")
return result
return wrapper
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
# Вручную оборачиваем функцию
greet = flexible_wrapper(greet)
result = greet("Alice")
# Output:
# Before calling the function
# After calling the function
print(result)
# Output: Hello, Alice!
result = greet("Bob", greeting="Hi")
# Output:
# Before calling the function
# After calling the function
print(result)
# Output: Hi, Bob!Как работают *args и **kwargs:
Как мы узнали в главе 20, *args и **kwargs позволяют функциям принимать переменное количество аргументов:
*argsсобирает все позиционные аргументы в кортеж(tuple)**kwargsсобирает все именованные аргументы в словарь(dictionary)- Когда мы вызываем
original_func(*args, **kwargs), мы распаковываем их обратно в аргументы для исходной функции
Этот шаблон позволяет нашей обёртке работать с любой функцией, независимо от того, сколько аргументов она принимает.
Переход к более чистому синтаксису
Этот шаблон — основа декораторов. Синтаксис декоратора, который мы изучим дальше, — это просто более чистый способ применить этот шаблон. Вместо того чтобы писать:
greet = flexible_wrapper(greet)Мы будем использовать синтаксис @:
@flexible_wrapper
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"Оба варианта делают ровно одно и то же — синтаксис @ лишь синтаксический сахар(syntactic sugar), который делает код чище и более читаемым.
38.3) Синтаксис @decorator: более удобное применение
Запись function_name = decorator(function_name) работает, но она громоздкая и о ней легко забыть. Python предоставляет синтаксис @decorator как более чистый способ применения декораторов.
Использование символа @
Вместо ручного оборачивания функции вы можете поместить @decorator_name на строку непосредственно перед определением функции:
def log_call(func):
"""Декоратор, который логирует вызовы функций."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@log_call
def calculate_total(prices):
return sum(prices)
@log_call
def find_average(numbers):
return sum(numbers) / len(numbers)
# Используем декорированные функции
total = calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60
print(f"Total: {total}")
# Output: Total: 60
average = find_average([10, 20, 30])
# Output:
# Calling find_average
# find_average returned: 20.0
print(f"Average: {average}")
# Output: Average: 20.0Синтаксис @log_call в точности эквивалентен записи:
def calculate_total(prices):
return sum(prices)
calculate_total = log_call(calculate_total)Но синтаксис @ гораздо чище и сразу делает очевидным, что функция декорирована.
Стек из нескольких декораторов
Вы можете применить несколько декораторов к одной и той же функции, выстраивая их друг над другом:
import time
def log_call(func):
"""Декоратор, который логирует вызовы функций."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
def timer(func):
"""Декоратор, который измеряет время выполнения функции."""
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start_time
print(f"{func.__name__} took {elapsed:.4f} seconds")
return result
return wrapper
@timer
@log_call
def process_data(items):
total = sum(items)
return total * 2
result = process_data([1, 2, 3, 4, 5])
# Output:
# Calling process_data
# process_data returned: 30
# process_data took 0.0001 seconds
print(f"Final result: {result}")
# Output: Final result: 30Когда декораторы «стекуются», они применяются снизу вверх (сначала ближайший к функции):
@timer # Применяется вторым (самый внешний слой)
@log_call # Применяется первым (ближе всего к функции)
def process_data(items):
passЭто эквивалентно:
process_data = timer(log_call(process_data))Порядок применения (снизу вверх):
@log_callсначала оборачивает исходную функцию@timerоборачивает результат (оборачивает уже обёрнутую функцию)
Порядок выполнения (сверху вниз, от внешнего к внутреннему):
- запускается обёртка
timer(внешняя, выполняется первой) - запускается обёртка
log_call(внутренняя обёртка) - выполняется исходная функция
- завершается обёртка
log_call - завершается обёртка
timer(внешняя, завершается последней)
Думайте о декораторах как о слоях обёрточной бумаги: вы накладываете их изнутри наружу, но когда разворачиваете (выполняете), вы идёте снаружи внутрь.
Применение декораторов:
Поток выполнения:
38.4) Практические примеры декораторов (логирование, замер времени, валидация)
Теперь давайте рассмотрим несколько практических декораторов, которые вы можете использовать в реальных программах. Эти примеры демонстрируют распространённые шаблоны и показывают, как декораторы решают реальные задачи.
Пример 1: Улучшенный декоратор логирования
Более продвинутый декоратор логирования, который включает временные метки и обрабатывает исключения:
import time
def log_with_timestamp(func):
"""Декоратор, который логирует вызовы функций с временными метками."""
def wrapper(*args, **kwargs):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] Calling {func.__name__}")
try:
result = func(*args, **kwargs)
print(f"[{timestamp}] {func.__name__} completed successfully")
return result
except Exception as e:
print(f"[{timestamp}] {func.__name__} raised {type(e).__name__}: {e}")
raise
return wrapper
@log_with_timestamp
def divide(a, b):
return a / b
@log_with_timestamp
def process_user(user_id):
# Имитация обработки
if user_id < 0:
raise ValueError("User ID must be positive")
return f"Processed user {user_id}"
# Тест успешного выполнения
result = divide(10, 2)
# Output:
# [2025-12-31 10:30:45] Calling divide
# [2025-12-31 10:30:45] divide completed successfully
print(f"Result: {result}")
# Output: Result: 5.0
# Тест успешного выполнения с валидацией
user = process_user(42)
# Output:
# [2025-12-31 10:30:45] Calling process_user
# [2025-12-31 10:30:45] process_user completed successfully
print(user)
# Output: Processed user 42
# Тест обработки исключений
try:
divide(10, 0)
# Output:
# [2025-12-31 10:30:45] Calling divide
# [2025-12-31 10:30:45] divide raised ZeroDivisionError: division by zero
except ZeroDivisionError:
print("Handled division by zero")
# Output: Handled division by zero
try:
process_user(-5)
# Output:
# [2025-12-31 10:30:45] Calling process_user
# [2025-12-31 10:30:45] process_user raised ValueError: User ID must be positive
except ValueError:
print("Handled invalid user ID")
# Output: Handled invalid user IDЭтот декоратор:
- Добавляет временные метки ко всем сообщениям логирования
- Логирует как успешное завершение, так и исключения
- Повторно выбрасывает исключения после логирования (используя
raiseбез аргумента) - Использует блок
try/exceptдля перехвата и логирования любого исключения
Пример 2: Декоратор замера производительности
Декоратор, который измеряет и сообщает время выполнения функции:
import time
def measure_time(func):
"""Декоратор, который измеряет и выводит время выполнения."""
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
# Форматируем время подходящим образом
if elapsed < 0.001:
time_str = f"{elapsed * 1000000:.2f} microseconds"
elif elapsed < 1:
time_str = f"{elapsed * 1000:.2f} milliseconds"
else:
time_str = f"{elapsed:.2f} seconds"
print(f"{func.__name__} executed in {time_str}")
return result
return wrapper
@measure_time
def find_primes(limit):
"""Найти все простые числа до limit."""
primes = []
for num in range(2, limit):
is_prime = True
for divisor in range(2, int(num ** 0.5) + 1):
if num % divisor == 0:
is_prime = False
break
if is_prime:
primes.append(num)
return primes
@measure_time
def calculate_factorial(n):
"""Вычислить факториал n."""
result = 1
for i in range(1, n + 1):
result *= i
return result
# Тест декорированных функций
primes = find_primes(1000)
# Output: find_primes executed in 15.23 milliseconds
print(f"Found {len(primes)} primes")
# Output: Found 168 primes
factorial = calculate_factorial(100)
# Output: calculate_factorial executed in 45.67 microseconds
print(f"Factorial has {len(str(factorial))} digits")
# Output: Factorial has 158 digitsЭтот декоратор автоматически форматирует измерение времени подходящим образом (микросекунды, миллисекунды или секунды) в зависимости от длительности.
Пример 3: Декоратор валидации ввода
Декоратор, который проверяет аргументы функции перед выполнением:
def validate_positive(func):
"""Декоратор, который гарантирует, что все числовые аргументы положительные."""
def wrapper(*args, **kwargs):
# Проверяем позиционные аргументы
for i, arg in enumerate(args):
if isinstance(arg, (int, float)) and arg <= 0:
raise ValueError(
f"Argument {i} to {func.__name__} must be positive, got {arg}"
)
# Проверяем именованные аргументы
for key, value in kwargs.items():
if isinstance(value, (int, float)) and value <= 0:
raise ValueError(
f"Argument '{key}' to {func.__name__} must be positive, got {value}"
)
return func(*args, **kwargs)
return wrapper
@validate_positive
def calculate_area(width, height):
"""Вычислить площадь прямоугольника."""
return width * height
@validate_positive
def calculate_discount(price, discount_percent):
"""Вычислить цену со скидкой."""
discount = price * (discount_percent / 100)
return price - discount
# Тест корректных входных данных
area = calculate_area(10, 5)
print(f"Area: {area}")
# Output: Area: 50
discounted = calculate_discount(100, 20)
print(f"Discounted price: ${discounted:.2f}")
# Output: Discounted price: $80.00
# Тест некорректных входных данных
try:
calculate_area(-5, 10)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: Argument 0 to calculate_area must be positive, got -5
try:
calculate_discount(100, discount_percent=-10)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: Argument 'discount_percent' to calculate_discount must be positive, got -10Этот декоратор:
- Проверяет все числовые аргументы (и позиционные, и именованные)
- Выбрасывает информативную ошибку, если какой-либо из них не положительный
- Выдаёт понятные сообщения об ошибках, указывающие, какой аргумент не прошёл валидацию
38.5) (Необязательно) Декораторы с аргументами
До сих пор все наши декораторы были простыми функциями, которые принимают функцию в качестве аргумента. Но что, если вы хотите настраивать поведение декоратора? Например, вам может понадобиться декоратор повторов, где вы можете указать число попыток, или декоратор логирования, где вы можете указать уровень логирования.
Декораторы с аргументами требуют дополнительного уровня вложенности функций. Вместо того чтобы декоратор был функцией, которая принимает функцию, он становится функцией, которая принимает аргументы и возвращает декоратор.
Шаблон: фабрики декораторов
Декоратор с аргументами на самом деле является фабрикой декораторов (decorator factory) — функцией, которая создаёт и возвращает декоратор. Ключ к пониманию здесь — знать, что Python делает с символом @.
Ключевой принцип: Python сначала вычисляет @
Python всегда сначала вычисляет всё, что стоит после @, а затем использует результат, чтобы декорировать вашу функцию.
Давайте сравним:
A) Базовый декоратор:
На основе этого примера:
def log_call(func):
"""Декоратор, который логирует вызовы функций."""
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"{func.__name__} returned: {result}")
return result
return wrapper
@log_call
def greet(name):
return f"Hello, {name}!"Что делает Python:
- Вычисляет
@log_call→ результат: самlog_call(объект функции) - Применяет к
greet:greet = log_call(greet)
B) Фабрика декораторов:
На основе этого примера:
def repeat(times):
"""Уровень 1: фабрика — получает конфигурацию"""
def decorator(func):
"""Уровень 2: декоратор — получает функцию для декорирования"""
def wrapper(*args, **kwargs):
"""Уровень 3: функция-обёртка — выполняется при вызове декорированной функции"""
for i in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello, {name}!")
greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!Что делает Python:
- Вычисляет
@repeat(3)→ результат:repeat(3)вызывается, возвращает функцию-декоратор - Применяет этот декоратор к
greet:greet = decorator(greet)
Разница: @log_call даёт вам саму функцию, а @repeat(3) вызывает функцию (repeat), которая возвращает декоратор.
Понимание трёх уровней
Фабрика декораторов имеет три вложенные функции, и у каждой — своя роль:
def repeat(times): # Уровень 1: фабрика
def decorator(func): # Уровень 2: декоратор
def wrapper(*args, **kwargs): # Уровень 3: обёртка
for i in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decoratorУровень 1 — фабрика (repeat):
- Принимает: конфигурацию (
times) - Возвращает: функцию-декоратор
- Вызывается: когда Python вычисляет
@repeat(3)
Уровень 2 — декоратор (decorator):
- Принимает: функцию для декорирования (
func) - Возвращает: функцию-обёртку
- Вызывается: сразу после уровня 1, как часть синтаксиса @
Уровень 3 — обёртка (wrapper):
- Принимает: аргументы функции при вызове (
*args, **kwargs) - Возвращает: результат
- Вызывается: каждый раз, когда вы вызываете декорированную функцию
Пошаговое выполнение
Давайте проследим, что происходит с @repeat(3):
# Что вы пишете:
@repeat(3)
def greet(name):
print(f"Hello, {name}!")Шаг 1: Python вычисляет repeat(3)
decorator = repeat(3) # Фабрика возвращает декоратор (times=3 захватывается)Шаг 2: Python применяет декоратор к greet
def greet(name):
print(f"Hello, {name}!")
greet = decorator(greet) # Декоратор возвращает обёртку (func=greet захватывается)Примечание: на этом этапе greet теперь ссылается на функцию-обёртку. Исходная greet захвачена в func.
Шаг 3: когда вы вызываете greet("Alice"), выполняется обёртка
greet("Alice") # На самом деле вызывает wrapper("Alice")
# wrapper использует захваченные 'times' и 'func'Почему три уровня?
Каждый уровень захватывает разную информацию через замыкания:
def repeat(times): # Захватывает: times
def decorator(func): # Захватывает: func (и помнит times)
def wrapper(*args, **kwargs): # Захватывает: times, func, и получает args
for i in range(times): # Использует захваченное 'times'
result = func(*args, **kwargs) # Использует захваченное 'func' и 'args'
return result
return wrapper
return decorator- Уровень 1 захватывает конфигурацию (
times) - Уровень 2 захватывает функцию для декорирования (
func) - Уровень 3 получает аргументы при вызове (
args,kwargs)
Без всех трёх уровней мы не смогли бы сделать настраиваемый декоратор, который помнит и свои настройки, и функцию, которую он декорирует.
Пример 1: Настраиваемый декоратор логирования
Вот практический пример декоратора логирования, который принимает конфигурацию:
def log_with_prefix(prefix="LOG"):
"""Фабрика декораторов, создающая декоратор логирования с пользовательским префиксом."""
def decorator(func):
def wrapper(*args, **kwargs):
print(f"[{prefix}] Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"[{prefix}] {func.__name__} returned: {result}")
return result
return wrapper
return decorator
@log_with_prefix(prefix="INFO")
def calculate_total(prices):
return sum(prices)
@log_with_prefix() # Используем префикс по умолчанию
def get_average(numbers):
return sum(numbers) / len(numbers)
# Тест декорированных функций
total = calculate_total([10, 20, 30])
# Output:
# [INFO] Calling calculate_total
# [INFO] calculate_total returned: 60
print(f"Total: {total}")
# Output: Total: 60
average = get_average([10, 20, 30])
# Output:
# [LOG] Calling get_average
# [LOG] get_average returned: 20.0
print(f"Average: {average}")
# Output: Average: 20.0Обратите внимание, что:
@log_with_prefix(prefix="INFO")использует пользовательский префикс@log_with_prefix()использует префикс по умолчанию "LOG"- Нужно включать скобки даже при использовании значений по умолчанию
Пример 2: Декоратор с несколькими аргументами
Вот декоратор, который валидирует числовые диапазоны:
def validate_range(min_value=None, max_value=None):
"""
Фабрика декораторов, которая проверяет, что числовые аргументы находятся в диапазоне.
Args:
min_value: Минимально допустимое значение (включительно)
max_value: Максимально допустимое значение (включительно)
"""
def decorator(func):
def wrapper(*args, **kwargs):
# Проверяем все числовые аргументы
all_args = list(args) + list(kwargs.values())
for arg in all_args:
if isinstance(arg, (int, float)):
if min_value is not None and arg < min_value:
raise ValueError(
f"{func.__name__} received {arg}, "
f"which is below minimum {min_value}"
)
if max_value is not None and arg > max_value:
raise ValueError(
f"{func.__name__} received {arg}, "
f"which is above maximum {max_value}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate_range(min_value=0, max_value=100)
def calculate_percentage(value, total):
"""Вычислить процент."""
return (value / total) * 100
@validate_range(min_value=0)
def calculate_age(birth_year, current_year):
"""Вычислить возраст по году рождения."""
return current_year - birth_year
# Тест корректных входных данных
percentage = calculate_percentage(25, 100)
print(f"Percentage: {percentage}%")
# Output: Percentage: 25.0%
age = calculate_age(1990, 2025)
print(f"Age: {age}")
# Output: Age: 35
# Тест некорректных входных данных
try:
calculate_percentage(150, 100)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: calculate_percentage received 150, which is above maximum 100
try:
calculate_age(-5, 2025)
except ValueError as e:
print(f"Validation error: {e}")
# Output: Validation error: calculate_age received -5, which is below minimum 0Когда использовать декораторы с аргументами
Используйте декораторы с аргументами, когда:
- вам нужно настраивать поведение декоратора
- один и тот же декоратор должен работать по-разному в разных контекстах
- вы хотите сделать декораторы более переиспользуемыми и гибкими
Распространённые примеры включают:
- декораторы повторов с настраиваемыми попытками и задержками
- декораторы логирования с настраиваемыми уровнями логирования или форматами
- декораторы валидации с настраиваемыми правилами
- декораторы кэширования с настраиваемыми размерами кэша или временем жизни
- ограничение частоты запросов с настраиваемыми лимитами
Замечание о сложности
Декораторы с аргументами добавляют дополнительный уровень сложности. При их написании:
- используйте ясные, описательные имена параметров
- задавайте разумные значения по умолчанию
- включайте docstring, объясняющие параметры
- подумайте, стоит ли дополнительная гибкость этой сложности
Для простых случаев декоратор без аргументов часто более понятен и его легче понять.
Декораторы (decorators) — мощный инструмент для написания чистого, поддерживаемого кода Python. Они позволяют отделять сквозные аспекты (такие как логирование, замер времени и валидация) от вашей основной бизнес-логики, делая код проще для чтения, тестирования и модификации. По мере того как вы продолжаете программировать на Python, вы будете часто встречать декораторы во фреймворках (frameworks) и библиотеках (libraries), и вы обнаружите множество возможностей писать собственные декораторы, чтобы элегантно решать распространённые проблемы.