Декораторы Python: Простые шаблоны для улучшения кода – Шпаргалка

Вы знаете это чувство, когда видите @something над функцией и гадаете, какая черная магия там происходит? Я тоже через это проходил. Декораторы могут показаться устрашающими, но на самом деле это одна из самых элегантных особенностей Python, как только вы поймете основы — см. Декораторы (шпаргалка) для краткого справочника.
Представьте себе декораторы как подарочную упаковку для ваших функций. Функция внутри остается прежней, но декоратор добавляет красивый бант сверху — дополнительную функциональность без изменения исходного кода.
Самый простой декоратор
Давайте начнем с самого базового примера, чтобы понять, что происходит:
def my_decorator(func):
def wrapper():
print("Something happens before!")
func()
print("Something happens after!")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
# Something happens before!
# Hello!
# Something happens after!
Вот и все! Декоратор — это просто функция, которая принимает другую функцию и оборачивает ее дополнительным поведением. Синтаксис @my_decorator — это просто более чистый способ написать say_hello = my_decorator(say_hello).
Ваш первый полезный декоратор: Таймер
Вот декоратор, который вы действительно захотите использовать — тот, который сообщает вам, сколько времени занимает выполнение ваших функций:
import time
import functools
def timer(func):
@functools.wraps(func) # Сохраняет имя и документацию исходной функции
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
return "Done!"
result = slow_function()
# slow_function took 1.0041 seconds
print(result) # Done!
См. Декораторы (шпаргалка) для дополнительных шаблонов декораторов и общих паттернов.
Видите, как мы используем *args и **kwargs (см. Аргументы и ключевые аргументы)? Это позволяет нашему декоратору работать с любой функцией, независимо от того, сколько аргументов она принимает.
Отладка вашего кода: Декоратор-логгер
Когда вы пытаетесь выяснить, что идет не так, этот декоратор невероятно полезен — также проверьте Отладка (шпаргалка) для сопутствующих советов и методов:
def debug(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
args_str = ', '.join(repr(arg) for arg in args)
kwargs_str = ', '.join(f"{k}={v!r}" for k, v in kwargs.items())
all_args = ', '.join(filter(None, [args_str, kwargs_str]))
print(f"Calling {func.__name__}({all_args})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result!r}")
return result
return wrapper
@debug
def add_numbers(a, b, multiply_by=1):
return (a + b) * multiply_by
result = add_numbers(5, 3, multiply_by=2)
# Calling add_numbers(5, 3, multiply_by=2)
# add_numbers returned 16
Управление доступом: Декоратор аутентификации
Хотите убедиться, что только определенные пользователи могут запускать функцию? Вот как это сделать:
def requires_auth(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# В реальном приложении вы бы проверяли фактическую аутентификацию
user_logged_in = True # Это пришло бы из вашей системы аутентификации
if not user_logged_in:
return "Access denied! Please log in."
return func(*args, **kwargs)
return wrapper
@requires_auth
def delete_everything():
return "💥 Everything deleted! (just kidding)"
result = delete_everything()
print(result) # 💥 Everything deleted! (just kidding)
Ускорение работы: Декоратор кэширования
Если у вас есть функция, которая выполняет дорогостоящие вычисления с одинаковыми входными данными, закэшируйте результаты:
def cache(func):
cached_results = {}
@functools.wraps(func)
def wrapper(*args):
if args in cached_results:
print(f"Cache hit for {func.__name__}{args}")
return cached_results[args]
print(f"Computing {func.__name__}{args}")
result = func(*args)
cached_results[args] = result
return result
return wrapper
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10))
# Computing fibonacci(10)
# Computing fibonacci(9)
# Computing fibonacci(8)
# ... (lots of computation)
# Cache hit for fibonacci(2)
# Cache hit for fibonacci(3)
# ... (cache hits)
# 55
Повтор неудачных операций
Иногда функции завершаются с ошибкой из-за проблем с сетью или временных проблем. Этот декоратор автоматически повторяет попытку:
import random
def retry(max_attempts=3):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Attempt {attempt + 1} failed: {e}")
if attempt == max_attempts - 1:
print("All attempts failed!")
raise
return wrapper
return decorator
@retry(max_attempts=3)
def unreliable_api_call():
if random.random() < 0.7: # 70% chance of failure
raise Exception("Network error")
return "Success!"
# Это повторит попытку до 3 раз, если произойдет сбой
result = unreliable_api_call()
Ограничение скорости: Замедлите ваш код
Иногда вам нужно быть осторожными с API или базами данных:
import time
import functools
def rate_limit(seconds):
"""
A decorator to limit how frequently a function can be called.
"""
def decorator(func):
# Используем список для хранения изменяемого значения float последнего времени вызова.
# Это позволяет внутренней функции-обертке его изменять.
last_called_at = [0.0]
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Вычисляем время, прошедшее с момента последнего вызова
elapsed = time.time() - last_called_at[0]
wait_time = seconds - elapsed
# Если прошло недостаточно времени, ждем остаток
if wait_time > 0:
time.sleep(wait_time)
# Обновляем время последнего вызова и выполняем функцию
last_called_at[0] = time.time()
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(1) # Разрешить не более одного вызова в секунду
def call_api():
print(f"API called at {time.time():.2f}")
# Эти вызовы будут разделены примерно на 1 секунду каждый
call_api()
call_api()
call_api()
# Ожидаемый вывод:
# API called at 1723823038.50
# API called at 1723823039.50
# API called at 1723823040.50
Проверка ваших входных данных
Убедитесь, что ваши функции получают данные правильного типа:
def validate_types(**expected_types):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Получаем имена параметров функции
import inspect
sig = inspect.signature(func)
bound_args = sig.bind(*args, **kwargs)
bound_args.apply_defaults()
for param_name, expected_type in expected_types.items():
if param_name in bound_args.arguments:
value = bound_args.arguments[param_name]
if not isinstance(value, expected_type):
raise TypeError(
f"{param_name} must be {expected_type.__name__}, "
f"got {type(value).__name__}"
)
return func(*args, **kwargs)
return wrapper
return decorator
@validate_types(name=str, age=int)
def create_user(name, age):
return f"User {name}, age {age}"
# Это работает
user1 = create_user("Alice", 25)
print(user1) # User Alice, age 25
# Это вызывает TypeError
try:
user2 = create_user("Bob", "twenty-five")
except TypeError as e:
print(e) # age must be int, got str
Когда использовать каждый декоратор
| Тип декоратора | Лучше всего подходит для | Примеры использования |
|---|---|---|
| Таймер | Мониторинг производительности | Поиск медленных функций, оптимизация |
| Отладчик/Логгер | Разработка и устранение неполадок | Понимание вызовов функций, отладка |
| Аутентификация | Безопасность и контроль доступа | Защита административных функций, права пользователей |
| Кэш | Дорогостоящие вычисления | Запросы к базе данных, вызовы API, сложные вычисления |
| Повтор | Ненадежные операции | Сетевые запросы, файловые операции |
| Ограничение скорости | Контроль частоты | Вызовы API, предотвращение спама |
| Валидация | Целостность данных | Ввод пользователя, параметры API |
Советы по использованию декораторов
Всегда используйте @functools.wraps — это сохраняет имя и документацию исходной функции, облегчая отладку (см. Шпаргалка по декораторам для примеров).
Сохраняйте их простыми — если ваш декоратор становится сложным, подумайте, не лучше ли сделать его классом или отдельной функцией.
Подумайте о порядке — при наложении декораторов тот, что ближе всего к функции, выполняется первым:
@timer
@debug
def my_function():
pass
# Это то же самое, что:
# my_function = timer(debug(my_function))
Не злоупотребляйте ими — декораторы мощные, но их слишком много может затруднить чтение кода.
Ключевые выводы
Декораторы позволяют добавлять функциональность к функциям, не изменяя их код. Они идеально подходят для сквозных задач, таких как измерение времени, ведение журналов, аутентификация и кэширование.
Начните с простых шаблонов, показанных здесь. Как только вы освоитесь, вы сможете создавать более сложные декораторы для ваших конкретных нужд. Главное — понять, что декораторы — это просто функции, которые оборачивают другие функции — все остальное является хитрым применением этой основной концепции.
Хотите попрактиковаться? Попробуйте добавить декоратор @timer к некоторым из ваших существующих функций и посмотрите, какие из них работают медленнее, чем вы ожидали. Вы можете удивиться тому, что обнаружите!
Связанные ссылки
Добавьте соответствующие внутренние ссылки на документацию ниже для дальнейшего чтения: