Как обработать исключения в декораторе, чтобы возвращать время выполнения и ошибку без прерывания выполнения?
Что проверяют
Понимаете ли вы, как устроен декоратор, и умеете ли гасить исключение осознанно, не теряя информацию о нём. Следом часто просят сделать то же для async-функции или добавить декоратору параметры.
Как отвечать
Каркас:
@dataclass
class Outcome:
elapsed: float
result: object = None
error: Exception | None = None
def safe_timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = error = None
try:
result = func(*args, **kwargs)
except Exception as exc:
error = exc
return Outcome(time.perf_counter() - start, result, error)
return wrapperЧто проговорить:
functools.wraps: сохраняет__name__,__doc__и__wrapped__, иначе в логах и трейсах видноwrapper;time.perf_counter(): монотонные часы высокого разрешения.time.time()может прыгнуть при синхронизации системного времени;except Exception, а не голыйexcept:KeyboardInterruptиSystemExitнаследуютBaseExceptionи должны пробрасываться;- объект исключения, а не
str(e): сохраняются тип и__traceback__, для записи в лог естьlogger.exception(); - корутины: нужна отдельная обёртка
async defсawait, выбор черезinspect.iscoroutinefunction.asyncio.CancelledErrorнаследуетBaseException, глотать его нельзя.
Скажите и о цене: декоратор меняет контракт, вызывающий получает Outcome вместо значения. Это уместно в пакетной обработке, где одна упавшая задача не должна останавливать остальные, но не в обычном коде.
Где спотыкаются
Считают время в двух местах, в try и в except, дублируя код. Достаточно одной точки после блока.
Возвращают str(e) в той же позиции, что и результат. Вызывающий не отличит ошибку от функции, которая честно вернула строку.
Вешают синхронный декоратор на async def. Замеряется только создание корутины, время около нуля, а исключение всплывёт позже на await, мимо обёртки.