Программирование PythonФункции и декораторыPython-разработчик серверной части

Как изменится поведение async функции, если её обернуть обычным синхронным декоратором?

Как изменится поведение async-функции, если её обернуть обычным синхронным декоратором?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Вызов обычной обёртки над async-функцией обычно возвращает объект-корутину, но не выполняет тело этой функции. Асинхронный код начнёт выполняться только после await корутины; поэтому синхронная обёртка не может корректно выполнить действия «после результата» без собственного асинхронного слоя.

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

Исторический контекст

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

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

Постановка проблемы

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

Например, исключение, возникшее внутри корутины, не будет поймано синхронным try вокруг выражения, которое только создаёт эту корутину. Если корутину затем никто не ожидает, она вообще не выполнится, а приложение может получить предупреждение о непогашенной корутине.

Подробное решение

Корректный вариант — сделать обёртку асинхронной и явно ожидать исходную функцию:

from functools import wraps def measure(fn): @wraps(fn) async def wrapper(*args, **kwargs): print("начало") try: return await fn(*args, **kwargs) finally: print("завершение") return wrapper @measure async def load(): return 42

Здесь load() возвращает корутину wrapper, а фактический вызов исходной функции происходит на await fn(...). Поэтому finally выполняется после завершения или отмены корутины, а исключения из асинхронного тела проходят через try.

Синхронный декоратор всё же может быть полезен, если его задача — только проверить аргументы до создания корутины или вернуть исходный результат без попытки его обработать. Однако такой декоратор не должен притворяться асинхронным мониторингом: он не видит длительность выполнения и исключения, возникающие после await.

functools.wraps сохраняет метаданные исходной функции, но не меняет модель выполнения. Само наличие __wrapped__, имени или аннотаций не превращает синхронную функцию-обёртку в асинхронную.

Ситуация из практики

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

Рассматривались два решения. Синхронная обёртка была проще и подходила бы для предварительной проверки аргументов, но не могла корректно охватить ожидание и исключения. Асинхронная обёртка требовала отдельной реализации, зато позволяла использовать await, try/finally и единообразно обрабатывать успешное завершение, ошибку и отмену.

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

Что кандидаты часто упускают

  1. Вопрос: Может ли синхронный декоратор над async-функцией быть корректным?

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

  2. Вопрос: Где возникнет исключение из async-функции при синхронном вызове без ожидания?

    Ответ: Не в момент создания корутины. Исключение возникнет позднее, когда корутину начнут выполнять через await или другой механизм запуска задачи. Поэтому синхронный try/except, окружающий только вызов async-функции, такое исключение обычно не перехватит.

  3. Вопрос: Достаточно ли объявить обёртку как async, если декоратор должен сохранить поведение исходной функции?

    Ответ: Не всегда. Обёртка должна передать позиционные и именованные аргументы, ожидать исходную функцию и корректно сохранить метаданные, обычно с помощью functools.wraps. Кроме того, нужно заранее определить поведение при отмене задачи, исключении и повторном вызове; простое добавление async решает только вопрос ожидания корутины.