Декораторы предоставляют новый и удобный способ для всего, от кэширования до отправки уведомлений.
Во-первых, цель каждого разработчика — заставить все работать. Постепенно мы беспокоимся о читабельности и масштабируемости. Именно тогда мы впервые начинаем думать о декораторах.
Декораторы — отличный способ придать функции дополнительное поведение. И есть небольшие вещи, которые нам, специалистам по данным, часто нужно вводить в определение функции.
С декораторами вы будете удивлены, увидев, насколько вы можете уменьшить повторение кода и улучшить читабельность. Я, конечно, сделал.
Вот пять наиболее распространенных из них, которые я использую почти в каждом проекте с интенсивным использованием данных.
1. Декоратор повторных попыток
В проектах по науке о данных и проектах по разработке программного обеспечения очень много случаев, когда мы зависим от внешних систем. Вещи не находятся под нашим контролем все время.
Когда происходит непредвиденное событие, мы можем захотеть, чтобы наш код подождал некоторое время, позволяя внешней системе исправить себя и перезапуститься.
Я предпочитаю реализовывать эту логику повтора внутри декоратора Python, чтобы я мог аннотировать любую функцию для применения поведения повтора.
Вот код декоратора повторных попыток.
import time from functools import wraps def retry(max_tries=3, delay_seconds=1): def decorator_retry(func): @wraps(func) def wrapper_retry(*args, **kwargs): tries = 0 while tries < max_tries: try: return func(*args, **kwargs) except Exception as e: tries += 1 if tries == max_tries: raise e time.sleep(delay_seconds) return wrapper_retry return decorator_retry @retry(max_tries=5, delay_seconds=2) def call_dummy_api(): response = requests.get("https://jsonplaceholder.typicode.com/todos/1") return response
В приведенном выше коде мы пытаемся получить ответ API. Если это не удается, мы повторяем ту же задачу 5 раз. Между каждой повторной попыткой ждем 2 секунды.
2. Кэширование результатов функции
Некоторые части нашей кодовой базы редко меняют свое поведение. Тем не менее, это может занять большую часть нашей вычислительной мощности. В таких ситуациях мы можем использовать декоратор для кэширования вызовов функций.
Функция будет запущена только один раз, если входные данные совпадают. При каждом последующем запуске результаты будут извлекаться из кеша. Следовательно, нам не нужно постоянно выполнять дорогостоящие вычисления.
def memoize(func): cache = {} def wrapper(*args): if args in cache: return cache[args] else: result = func(*args) cache[args] = result return result return wrapper
Декоратор использует словарь, сохраняет аргументы функции и возвращает значения. Когда мы выполним эту функцию, декорированный проверит словарь на наличие предыдущих результатов. Фактическая функция вызывается только тогда, когда ранее не было сохраненного значения.
Ниже приведено число Фибоначчи, вычисляющее функцию. Поскольку это рекуррентная функция, одна и та же вызываемая функция выполняется несколько раз. Но с помощью кэширования мы можем ускорить этот процесс.
@memoize def fibonacci(n): if n <= 1: return n else: return fibonacci(n-1) + fibonacci(n-2)
Вот время выполнения этой функции с кэшированием и без него. Обратите внимание, что для запуска кешированной версии требуется всего доля миллисекунды, тогда как для некэшированной версии требуется почти минута.
Function slow_fibonacci took 53.05560088157654 seconds to run. Function fast_fibonacci took 7.772445678710938e-05 seconds to run.
Использование словаря для хранения данных предыдущего выполнения является простым подходом. Однако существует более сложный способ хранения данных кэширования. Вы можете использовать базу данных в памяти, например Redis.
3. Временные функции
Это не удивительно. При работе с функциями, интенсивно использующими данные, нам не терпится узнать, сколько времени требуется для их выполнения.
Обычный способ сделать это — собрать две метки времени, одну в начале и другую в конце функции. Затем мы можем вычислить продолжительность и распечатать ее вместе с возвращаемыми значениями.
Но делать это снова и снова для нескольких функций — проблема.
Вместо этого мы можем попросить сделать это декоратора. Мы можем аннотировать любую функцию, которая требует вывода длительности.
Вот пример декоратора Python, который печатает время выполнения функции при ее вызове:
import time def timing_decorator(func): def wrapper(*args, **kwargs): start_time = time.time() result = func(*args, **kwargs) end_time = time.time() print(f"Function {func.__name__} took {end_time - start_time} seconds to run.") return result return wrapper
Вы можете использовать этот декоратор для определения времени выполнения функции:
@timing_decorator def my_function(): # some code here time.sleep(1) # simulate some time-consuming operation return
Вызов функции напечатает время, необходимое для запуска.
my_function() >>> Function my_function took 1.0019128322601318 seconds to run.
4. Регистрация вызовов функций
Этот декоратор во многом является расширением предыдущего декоратора. Но у него есть некоторые особенности использования.
Если вы следуете принципам проектирования программного обеспечения, вы оцените принцип единой ответственности. По сути, это означает, что каждая функция будет иметь свою одну и только одну ответственность.
Когда вы проектируете свой код таким образом, вы также хотите регистрировать информацию о выполнении ваших функций. Вот тут-то и пригодятся декораторы журналирования.
Следующий пример иллюстрирует это.
import logging import functools logging.basicConfig(level=logging.INFO) def log_execution(func): @functools.wraps(func) def wrapper(*args, **kwargs): logging.info(f"Executing {func.__name__}") result = func(*args, **kwargs) logging.info(f"Finished executing {func.__name__}") return result return wrapper @log_execution def extract_data(source): # extract data from source data = ... return data @log_execution def transform_data(data): # transform data transformed_data = ... return transformed_data @log_execution def load_data(data, target): # load data into target ... def main(): # extract data data = extract_data(source) # transform data transformed_data = transform_data(data) # load data load_data(transformed_data, target)
Приведенный выше код представляет собой упрощенную версию конвейера ETL. У нас есть три отдельные функции для обработки каждого извлечения, преобразования и загрузки. Мы обернули каждый из них с помощью нашего декоратора log_execution
.
Теперь всякий раз, когда код выполняется, вы увидите вывод, подобный этому:
INFO:root:Executing extract_data INFO:root:Finished executing extract_data INFO:root:Executing transform_data INFO:root:Finished executing transform_data INFO:root:Executing load_data INFO:root:Finished executing load_data
Мы также могли бы напечатать время выполнения внутри этого декоратора. Но я хотел бы иметь их обоих в отдельных декораторах. Таким образом, я могу выбрать, какой из них (или оба) использовать для функции.
Вот как можно использовать несколько декораторов в одной функции.
@log_execution @timing_decorator def my_function(x, y): time.sleep(1) return x + y
5. Декоратор уведомлений
Наконец, очень полезным декоратором в производственных системах является декоратор уведомлений.
Опять же, даже при нескольких повторных попытках даже хорошо протестированная кодовая база терпит неудачу. И когда это происходит, нам нужно сообщить кому-то об этом, чтобы принять быстрые меры.
Это не ново, если вы когда-либо строили конвейер данных и надеялись, что он будет работать вечно.
Следующий декоратор отправляет электронное письмо всякий раз, когда выполнение внутренней функции завершается сбоем. В вашем случае это не обязательно должно быть уведомление по электронной почте. Вы можете настроить его для отправки уведомлений Teams/slack.
import smtplib import traceback from email.mime.text import MIMEText def email_on_failure(sender_email, password, recipient_email): def decorator(func): def wrapper(*args, **kwargs): try: return func(*args, **kwargs) except Exception as e: # format the error message and traceback err_msg = f"Error: {str(e)}\n\nTraceback:\n{traceback.format_exc()}" # create the email message message = MIMEText(err_msg) message['Subject'] = f"{func.__name__} failed" message['From'] = sender_email message['To'] = recipient_email # send the email with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp: smtp.login(sender_email, password) smtp.sendmail(sender_email, recipient_email, message.as_string()) # re-raise the exception raise return wrapper return decorator @email_on_failure(sender_email='[email protected]', password='your_password', recipient_email='[email protected]') def my_function(): # code that might fail
Заключение
Декораторы — это очень удобный способ применить новое поведение к нашим функциям. Без них будет много повторений кода.
В этом посте я обсудил наиболее часто используемые декораторы. Вы можете расширить их для своих конкретных потребностей. Например, вы можете использовать сервер Redis для хранения ответов кеша вместо словарей. Это даст вам больше контроля над данными, такими как постоянство. Или вы можете настроить код, чтобы постепенно увеличивать время ожидания в декораторе повторных попыток.
Во всех своих проектах я использую некоторые версии этих декораторов. Хотя их поведение немного отличается, это общие цели, для которых я часто использую декораторы.
Я надеюсь, что этот пост поможет вам.
Спасибо за прочтение, друг! Если вам понравилась моя статья, давайте поддерживать связь в LinkedIn, Twitter и Medium.
Еще не являетесь участником Medium? Пожалуйста, используйте эту ссылку, чтобы стать участником, потому что без каких-либо дополнительных затрат для вас я получаю небольшую комиссию за рекомендацию вас.