← все задачи

Python · задача 7 из 10

Контекстный менеджер для замера времени

Средний 15 минут контекстные менеджерыcontextlibисключения

Условие

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

Что требуется

  • Работает как with timer("загрузка"):
  • При исключении время всё равно записывается, исключение не подавляется
  • Замеренное время доступно снаружи блока

Пример

with timer("запрос в БД") as t:
    rows = query()

print(t.elapsed)   # 0.42
# лог: запрос в БД: 0.420с

Сначала уточните

Вопросы до кода — половина оценки. Молча начать печатать хуже, чем задать два вопроса.

  • Логировать или возвращать значение — куда должен уходить результат?
  • Нужен ли вариант как декоратор для функции целиком?
  • Какая точность нужна и годится ли monotonic-часы?
Показать решение Скрыть решение

Решение

import logging
import time
from contextlib import contextmanager

logger = logging.getLogger(__name__)


class timer:
    def __init__(self, label):
        self.label = label
        self.elapsed = None

    def __enter__(self):
        self.started = time.perf_counter()
        return self                      # чтобы работало "as t"

    def __exit__(self, exc_type, exc, tb):
        self.elapsed = time.perf_counter() - self.started
        logger.info("%s: %.3fс%s", self.label, self.elapsed,
                    " (с ошибкой)" if exc_type else "")
        return False                     # False — исключение летит дальше


# Короткий вариант, если объект наружу не нужен:
@contextmanager
def timer_cm(label):
    started = time.perf_counter()
    try:
        yield
    finally:
        logger.info("%s: %.3fс", label, time.perf_counter() - started)

Почему так

Почему __exit__ возвращает False

  • Истинное значение из __exit__ подавляет исключение — блок «проглотит» ошибку, и это молчаливая потеря данных
  • return False (или просто ничего) означает «я прибрался, исключение пусть летит дальше»
  • Это ровно тот вопрос, ради которого задачу и дают: разницу между «прибраться» и «проглотить» понимают не все

Почему perf_counter, а не time.time

  • time.time берёт системные часы: перевод времени или синхронизация NTP могут дать отрицательный интервал
  • perf_counter — монотонные часы с максимальным разрешением, они ровно для замеров и созданы
  • Для измерения процессорного времени вместо реального есть process_time — уточнение, которое любят

Почему try/finally в варианте с contextmanager

  • Без finally при исключении внутри блока код после yield не выполнится и замер потеряется
  • finally гарантирует, что время запишется в любом случае, а исключение продолжит путь наверх
  • Класс против @contextmanager — вопрос вкуса, но объект нужен, когда наружу отдаётся состояние (elapsed)

Что спросят дальше

  • Спросят, как сделать из этого ещё и декоратор: наследование от ContextDecorator даёт это бесплатно
  • Спросят про async with — это __aenter__/__aexit__ или @asynccontextmanager
  • Могут попросить складывать метрику в Prometheus или statsd — покажите, что знаете, куда такие замеры уходят в проде

Следующая задача

LRU-кэш своими руками — Проверяют структуры данных и понимание, почему обычный словарь тут не спасает.