Этот вопрос проверяет умение создавать универсальные декораторы в Python, которые могут работать как с обычными, так и с асинхронными функциями, что важно для написания гибкого и переиспользуемого кода.
В Python декоратор — это функция, которая принимает другую функцию и возвращает новую, обычно добавляя какую-то дополнительную логию. С появлением асинхронного программирования (async/await) часто возникает необходимость, чтобы один и тот же декоратор мог работать как с обычными синхронными функциями, так и с асинхронными. Ключевая идея заключается в том, чтобы внутри обёртки (wrapper) определить, что вернул вызов исходной функции: обычное значение или корутину (объект типа coroutine).
Создадим функцию-декоратор universal_decorator. Внутри неё определим обёртку. При вызове обёртки мы сначала выполняем целевую функцию (с переданными аргументами) и получаем результат. Затем проверяем, является ли этот результат "awaitable" (например, с помощью inspect.iscoroutine()). Если да, то нам нужно вернуть асинхронную обёртку, которая будет ждать этот результат. Поэтому сама обёртка тоже должна быть асинхронной функцией. Но как сделать одну обёртку и для синхронного, и для асинхронного случая? Часто используют такой приём: создают две разные внутренние функции или делают саму обёртку асинхронной, но это может быть неэффективно для синхронных вызовов. Более элегантный способ — определить обёртку, которая проверяет тип результата и соответственно себя ведёт.
Вот практический пример декоратора, который замеряет время выполнения функции и работает с обоими типами функций:
import asyncio
import time
from functools import wraps
from inspect import iscoroutinefunction
def timer_decorator(func):
@wraps(func)
def sync_wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} took {elapsed:.4f} seconds (sync)")
return result
@wraps(func)
async def async_wrapper(*args, **kwargs):
start = time.time()
result = await func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} took {elapsed:.4f} seconds (async)")
return result
if iscoroutinefunction(func):
return async_wrapper
else:
return sync_wrapper
# Пример использования
@timer_decorator
def sync_func():
time.sleep(1)
return "done sync"
@timer_decorator
async def async_func():
await asyncio.sleep(1)
return "done async"
# Синхронный вызов
print(sync_func()) # Выведет время и "done sync"
# Асинхронный вызов
print(asyncio.run(async_func())) # Выведет время и "done async"В этом примере мы используем inspect.iscoroutinefunction() для проверки, является ли декорируемая функция асинхронной. В зависимости от этого возвращаем либо синхронную, либо асинхронную обёртку. Это эффективно, потому что для синхронной функции не создаются лишние асинхронные конструкции.
Такой подход полезен в библиотеках и фреймворках, где декораторы предоставляют общую функциональность (логирование, кэширование, проверка прав доступа, повторные попытки) и должны работать прозрачно как в синхронном, так и в асинхронном контексте. Например, в веб-фреймворках типа FastAPI или aiohttp могут быть декораторы маршрутов, которые обрабатывают и синхронные, и асинхронные обработчики.
Вывод: Универсальные декораторы, поддерживающие оба типа функций, стоит применять, когда вы пишете библиотечный код или инструменты, которые будут использоваться в смешанной синхронно-асинхронной среде, чтобы избежать дублирования логики и повысить переиспользуемость.