Асинхронный декоратор медленных вызовов

47. Асинхронный декоратор медленных вызовов

Условие здаадчи:
Дан асинхронный код. Нужно реализовать декоратор slowlog(threshold=3), который измеряет время выполнения асинхронной функции.

Требования:

если функция выполняется быстрее или ровно за threshold секунд, логирование не выполняется если функция выполняется дольше threshold секунд, нужно что-то залогировать декоратор должен корректно работать с async def функциями возвращаемое значение исходной функции должно сохраняться без изменений

import asyncio

@slowlog(threshold=3)
async def some_func(seconds):
    await asyncio.sleep(seconds)
    return seconds * 2

assert asyncio.run(some_func(1)) == 2  # не должно логгироваться
assert asyncio.run(some_func(5)) == 10  # должно логгироваться

# После превышения threshold секунд, мы логгируем что-то
Спойлеры к решению
Подсказки
  • Декоратор должен принимать параметр threshold, значит нужен декоратор-фабрика.
  • Так как декорируемая функция асинхронная, обёртка тоже должна быть async def.
  • Внутри обёртки нужно вызвать исходную функцию через await.
  • Для измерения времени лучше использовать time.perf_counter().
  • Если время выполнения больше threshold, нужно выполнить логирование.
  • Возвращаемое значение исходной функции нужно вернуть без изменений.
  • Чтобы сохранить имя и метаданные функции, стоит использовать functools.wraps.
Решение
import asyncio
import time
from functools import wraps


def slowlog(threshold: float = 3):
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            start_time = time.perf_counter()

            result = await func(*args, **kwargs)

            end_time = time.perf_counter()
            duration = end_time - start_time

            if duration > threshold:
                print(
                    f"Slow function: {func.__name__} "
                    f"worked {duration:.2f} seconds"
                )

            return result

        return wrapper

    return decorator

Пример использования:

@slowlog(threshold=3)
async def some_func(seconds):
    await asyncio.sleep(seconds)
    return seconds * 2


assert asyncio.run(some_func(1)) == 2   # не логгируется
assert asyncio.run(some_func(5)) == 10  # логгируется

Примерный вывод для второго вызова:

Slow function: some_func worked 5.00 seconds

Декоратор slowlog(threshold=3) сначала возвращает обычный декоратор decorator, а тот уже принимает исходную функцию func.

Так как some_func является асинхронной функцией, внутри wrapper обязательно используется:

result = await func(*args, **kwargs)

Без await функция не была бы выполнена нормально, а вместо результата вернулась бы coroutine.

Проверка времени выполняется после завершения функции:

if duration > threshold:
    print(...)

По условию логирование должно быть только если функция работает дольше threshold. Поэтому при duration == threshold логирования не будет.

Сложность по времени самого декоратора — O(1), если не учитывать время работы исходной функции. По памяти — O(1).