Реализуйте контекстный менеджер Timer двумя способами: классом и через contextlib.
Короткий ответ
- Классовый вариант: __enter__ запускает отсчёт, __exit__ печатает
- __enter__ возвращает self, чтобы читать результат через as
- __exit__ не возвращает True — исключения пробрасываются
- Вариант contextlib: генератор с yield внутри try/finally
- time.perf_counter() точнее time.time() для замеров
- Замер должен сработать и при исключении в блоке
Оба варианта фиксируют время в точке входа и считают длительность при любом выходе из блока; классовый даёт объект с результатом, генераторный короче.
Как сказать вслух
пример ответаВ классовом варианте я запускаю отсчёт в enter и возвращаю self, чтобы через as можно было прочитать длительность, а в exit считаю разницу — он вызовется даже при исключении, и подавлять его я не буду. Во втором варианте пишу функцию-генератор с одним yield под декоратором contextmanager, а замер кладу в try/finally. Для времени беру perf_counter — он монотонный и не прыгает при переводе системных часов.
Подробный ответ
Основной ответ
Классовая реализация: __enter__ сохраняет time.perf_counter() и возвращает self — тогда with Timer() as t даёт объект, у которого после блока доступен t.elapsed. __exit__(exc_type, exc, tb) вычисляет разницу и возвращает None (ложь), чтобы исключения из блока шли наружу; сам факт его вызова гарантирован при любом выходе. Генераторная версия через @contextmanager компактнее: код до yield — вход, finally после — выход; yield может отдать изменяемый объект (например, словарь), в который finally допишет результат. perf_counter выбирают потому, что это монотонные часы высокого разрешения, не зависящие от перевода системного времени. Задача демонстрирует эквивалентность двух стилей и понимание семантики __exit__.
Ключевые моменты
- Возврат self из __enter__. Иначе as-переменная будет None и результат замера некуда положить.
- Семантика __exit__. Ложный возврат пробрасывает исключение; замер при этом всё равно фиксируется.
- try/finally вокруг yield. В генераторной версии без finally исключение в блоке отменит код «после выхода».
- perf_counter. Монотонный таймер для интервалов; time.time() может скакнуть из-за NTP-синхронизации.
Практический контекст
Такой таймер — реальный утилитарный код: замер запросов к БД, внешних вызовов, этапов пайплайна перед передачей в метрики. На собеседовании задача проверяет знание протокола контекстных менеджеров и аккуратность с исключениями; сильный кандидат сам проговаривает, почему __exit__ не должен возвращать True и чем perf_counter лучше time.
Пример кода
import time
from contextlib import contextmanager
class Timer:
def __enter__(self):
self._start = time.perf_counter()
return self
def __exit__(self, exc_type, exc, tb):
self.elapsed = time.perf_counter() - self._start
return None # исключения не подавляем
@contextmanager
def timer():
start = time.perf_counter()
result = {}
try:
yield result
finally:
result["elapsed"] = time.perf_counter() - start
with Timer() as t:
sum(range(10**6))
print(t.elapsed)Частые ошибки
- Не возвращают self из __enter__, и переменная после as оказывается None
- Возвращают True из __exit__ и молча проглатывают исключения блока
- В генераторной версии пишут код после yield без finally — при ошибке замер теряется