[Перевод] Пять декораторов Python, которые могут сократить код в два раза

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

ea5ffc46f06aa877a5e8665b6aa5cb69.png

Оболочки Python

Оболочки Python позволяют добавить новую функциональность или модифицировать ее поведение без непосредственного изменения исходного кода. 

Оболочки можно использовать в различных сценариях:

Расширение функциональности: Мы можем задействовать декоратор и добавить такие функции, как ведение журнала, измерение производительности или кэширование.

Многократное использование кода: Мы можем применить функцию-декоратор к нескольким элементам: благодаря этому можно избежать дублирования кода и достичь согласованного поведения различных компонентов.

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

Примеры:

1 — Timer

Эта функция измеряет время выполнения операции и выводит прошедшее время. Её можно использовать  для анализа кода и его оптимизации .

import time

def timer(func):
    def wrapper(*args, **kwargs):
        # start the timer
        start_time = time.time()
        # call the decorated function
        result = func(*args, **kwargs)
        # remeasure the time
        end_time = time.time()
        # compute the elapsed time and print it
        execution_time = end_time - start_time
        print(f"Execution time: {execution_time} seconds")
        # return the result of the decorated function execution
        return result
    # return reference to the wrapper function
    return wrapper

Для создания декоратора в Python нам необходимо определить функцию timer, которая получает параметр func. Это указывает на то, что это декораторская функция. Внутри функции timer мы определяем другую функцию — обертку, которая принимает аргументы, обычно передаваемые функции, которую мы хотим «декорировать».

Внутри функции-обертки мы запускаем нужную функцию, используя указанные аргументы. Это можно сделать с помощью строки: result = func(*args, **kwargs).

Функция-декоратор возвращает запрос на функцию-обертку, которую мы только что создали.

Чтобы использовать декоратор, можно применить его к нужной функции с помощью символа @.

@timer
def train_model():
    print("Starting the model training function...")
    # simulate a function execution by pausing the program for 5 seconds
    time.sleep(5) 
    print("Model training completed!")

train_model() 

Что получим в итоге:

Starting the model training function…

Model Training completed!

Execution time: 5.006425619125366 seconds

2 — debug

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

def debug(func):
    def wrapper(*args, **kwargs):
        # print the fucntion name and arguments
        print(f"Calling {func.__name__} with args: {args} kwargs: {kwargs}")
        # call the function
        result = func(*args, **kwargs)
        # print the results
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper

Мы можем использовать параметры __name__ для того, чтобы получить имя вызываемой функции, а затем параметры args, kwargs для вывода того, что было предано функции.

@debug
def add_numbers(x, y):
    return x + y
add_numbers(7, y=5,)  # Output: Calling add_numbers with args: (7) kwargs: {'y': 5} \n add_numbers returned: 12

3 — Exception Handler

Функция exception_handler будет ловить любые исключения, возникающие в функции-обертке, и обрабатывать их в зависимости от ситуации.

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

def exception_handler(func):
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            # Handle the exception
            print(f"An exception occurred: {str(e)}")
            # Optionally, perform additional error handling or logging
            # Reraise the exception if needed
    return wrapper

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

@exception_handler
def divide(x, y):
    result = x / y
    return result
divide(10, 0)  # Output: An exception occurred: division by zero

4 — Input Validator

Эта функция-обертка проверяет исходные аргументы функции на соответствие заданным условиям или типам данных. Она может быть использована для обеспечения корректности и согласованности входных данных.

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

Сама функция validate_input теперь выступает в роли декоратора. Внутри функции-обертки входные данные и аргументы ключевых слов проверяются на соответствие предоставленным функциям проверки. Если какой-либо аргумент не проходит проверку, то возникает ошибка ValueError с сообщением о недопустимости аргумента.

def validate_input(*validations):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for i, val in enumerate(args):
                if i < len(validations):
                    if not validations[i](val):
                        raise ValueError(f"Invalid argument: {val}")
            for key, val in kwargs.items():
                if key in validations[len(args):]:
                    if not validations[len(args):][key](val):
                        raise ValueError(f"Invalid argument: {key}={val}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

Для запуска валидированного ввода необходимо определить функции валидации. Например, можно использовать две функции проверки. Первая функция (lambda x: x > 0) проверяет, что аргумент x больше 0, а вторая функция (lambda y: isinstance(y, str)) проверяет, что аргумент y имеет тип string.

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

@validate_input(lambda x: x > 0, lambda y: isinstance(y, str))
def divide_and_print(x, message):
    print(message)
    return 1 / x

divide_and_print(5, "Hello!")  # Output: Hello! 1.0

5 — Retry

Эта обёртка позволяет повторить запуск функции заданное количество раз с задержкой между повторами — удобно при работе с сетевыми или API-вызовами, которые не выполняются из-за каких-либо проблем.

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

При запуске декорированной функции вызывается функция-обертка. Она отслеживает количество попыток (начиная с 0) и переходит в цикл while. Цикл пытается выполнить декорированную функцию и в случае успеха немедленно посылает результат. Если же произошло какое-то отклонение, то цикл увеличивает счетчик попыток и выводит сообщение об ошибке с указанием номера попытки и конкретного отклонения. Затем цикл ждет заданную задержку с помощью функции time.sleep, после чего повторяет попытку выполнения функции.

import time

def retry(max_attempts, delay=1):
    def decorator(func):
        def wrapper(*args, **kwargs):
            attempts = 0
            while attempts < max_attempts:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    attempts += 1
                    print(f"Attempt {attempts} failed: {e}")
                    time.sleep(delay)
            print(f"Function failed after {max_attempts} attempts")
        return wrapper
    return decorator

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

@retry(max_attempts=3, delay=2)
def fetch_data(url):
    print("Fetching the data..")
    # raise timeout error to simulate a server not responding..
    raise TimeoutError("Server is not responding.")
fetch_data("https://example.com/data")  # Retries 3 times with a 2-second delay between attempts

От редакции

28 августа начнется новый поток по языку программирования Python. На нем мы разберем: Библиотеки Python и решение конкретных задач DevOps; Правила эффективного и поддерживаемого кода; Принципы автоматизации: Docker, Gitlab, Prometheus, K8S и многое другое.

Узнать больше о потоке вы можете на нашем сайте: ссылка

© Habrahabr.ru