К чему приведёт объявление фабрики декоратора как async-функции вместо обычной функции?
При вызове async-фабрики декоратора возвращается не декоратор, а объект-корутина. Конструкция декорирования затем пытается использовать этот объект как вызываемый декоратор, что обычно приводит к TypeError; дополнительно может появиться предупреждение о непогашённой корутине.
Синтаксис декораторов предназначен для преобразования функции в момент её определения. Фабрика декоратора сначала получает параметры, затем возвращает обычную функцию-декоратор, которая принимает целевую функцию и возвращает замену.
Асинхронная функция устроена иначе: её вызов не выполняет тело сразу, а создаёт корутину, которую нужно запланировать или ожидать. Поэтому async меняет контракт фабрики: вместо вызываемого объекта для декорирования она возвращает отложенное вычисление.
При записи декоратора с параметрами Python сначала вычисляет выражение фабрики, затем передаёт целевую функцию полученному результату. Если результатом оказывается корутина, она не поддерживает вызов как обычный декоратор.
Неверное решение ломает декорирование ещё при создании функции или класса, то есть ошибка возникает до первого вызова целевой функции. Это особенно опасно при импорте модуля: приложение может не запуститься, а предупреждение о корутине способно замаскировать первопричину.
Обычная фабрика декоратора должна быть синхронной: она получает параметры и немедленно возвращает функцию-декоратор. Уже сама обёртка может быть асинхронной, если ей нужно ожидать выполнение исходной функции.
Здесь retry_decorator синхронно возвращает decorate, поэтому декорирование проходит корректно. wrapper является асинхронной только потому, что ей нужно выполнить await над исходной функцией.
Если фабрике действительно требуется асинхронная инициализация, её нельзя неявно выполнять в момент применения обычного декоратора. Обычно асинхронную подготовку выносят в явный этап запуска приложения, используют заранее созданное состояние или выполняют подготовку внутри первой асинхронной обёртки.
Важно отличать асинхронную фабрику от асинхронной обёртки. Первая возвращает корутину вместо декоратора, а вторая возвращает обычный вызываемый объект — асинхронную функцию, которую вызывающий код затем должен ожидать.
В веб-сервисе понадобился декоратор, который перед обработчиком проверяет доступность асинхронного сервиса авторизации. Разработчик сделал фабрику async, рассчитывая дождаться подключения при применении декоратора, но приложение стало падать при импорте обработчиков.
Рассматривались два варианта. Можно было оставить асинхронную фабрику и вручную ожидать её результат, но обычный синтаксис декоратора не предоставляет для этого места: импорт модуля не является асинхронным этапом. Другой вариант — сделать фабрику синхронной, а проверку подключения выполнять внутри асинхронной обёртки; это добавляет проверку при вызове, зато сохраняет корректный контракт декоратора.
Выбран второй вариант, а создание клиента авторизации вынесли в явный этап запуска приложения. В результате модули импортируются без побочных асинхронных операций, а обработчик получает понятную ошибку сервиса во время выполнения запроса.
1. Чем отличается ошибка у async-фабрики от ошибки у async-декоратора без параметров?
У фабрики выражение вроде @factory(settings) сначала вызывает асинхронную функцию и получает корутину, после чего Python пытается применить её к целевой функции. У декоратора без параметров @decorator сама функция передаётся в async-функцию, и имя целевой функции заменяется возвращённой корутиной. В обоих случаях контракт нарушен, но момент и форма последующей ошибки различаются.
2. Можно ли сделать асинхронной саму функцию, которую возвращает фабрика?
Да. Фабрика и функция-декоратор должны оставаться обычными функциями, а возвращаемая ими обёртка может быть объявлена через async def. Тогда применение декоратора происходит синхронно, а асинхронность проявляется только при вызове обёрнутой функции.
3. Почему нельзя просто ожидать корутину фабрики внутри синхронного декоратора?
Для ожидания нужен работающий цикл событий, а при импорте модуля его может не быть. Даже если запускать цикл событий вручную, это создаёт проблемы при уже работающем цикле, блокирует поток и смешивает этап конфигурации с выполнением приложения. Надёжнее выполнить асинхронную инициализацию явно при старте и передать готовое состояние синхронной фабрике.