Как обработать ошибку в функции внутри декоратора и вывести время работы функции в любом случае
Что проверяют
Знаете ли вы про finally и functools.wraps и понимаете ли, что исключение нельзя проглатывать.
Как отвечать
Задача решается связкой try/except/finally: замер печатается в finally, поэтому выполняется и при успехе, и при исключении, и при досрочном возврате.
import functools
import logging
import time
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
except Exception:
logging.exception("ошибка в %s", func.__name__)
raise
finally:
elapsed = time.perf_counter() - start
print(f"{func.__name__}: {elapsed:.3f} с")
return wrapperЧто здесь важно объяснить по пунктам:
finallyвыполняется всегда, поэтому время печатается и при ошибке. Это ядро задачи;raiseбез аргумента пробрасывает то же исключение с исходной трассировкой. Писатьraise eхуже: трассировка обрезается. Проглатывать исключение нельзя вовсе, иначе вызывающий код решит, что всё прошло успешно;logging.exceptionвнутри блокаexceptсам пишет трассировку, отдельныйtracebackне нужен;functools.wrapsсохраняет имя, документацию и сигнатуру обёрнутой функции. Без негоfunc.__name__станетwrapper, сломаются документация, интроспекция и часть библиотек;time.perf_counter, а неtime.time: первый монотонный и не прыгает при переводе часов, второй показывает календарное время;- *`args, kwargs` позволяют декорировать функцию с любой сигнатурой.
Полезно добавить два расширения. Для асинхронной функции нужен свой вариант с async def wrapper и await func(...), потому что обычная обёртка вернёт корутину и замерит время её создания, а не выполнения. А если декоратору нужны параметры (например порог для предупреждения), добавляется ещё один уровень вложенности.
Где спотыкаются
Печатают время только в успешной ветке.
Проглатывают исключение вместо повторного возбуждения.
Забывают functools.wraps.
Используют time.time для измерения длительности.