← Назад к вопросам
Параметризованный декоратор замера времени
1.7 Middle🔥 201 комментариев
#Python Core#Архитектура и паттерны
Условие
Напишите параметризованный декоратор, который печатает время выполнения декорированной функции.
Параметр декоратора — единицы измерения: секунды или миллисекунды.
Пример
@timer(unit="ms") def slow_function(): time.sleep(1)
slow_function()
Вывод: slow_function took 1000.0 ms
@timer(unit="s") def slow_function(): time.sleep(1)
slow_function()
Вывод: slow_function took 1.0 s
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Параметризованный декоратор замера времени
Декораторы — важная часть Python. Параметризованный декоратор (decorator factory) — это функция, которая принимает параметры и возвращает сам декоратор. Это задача проверяет понимание замыканий (closures) и функционального программирования.
Решение 1: Базовое
import time
import functools
from typing import Callable, Any
def timer(unit: str = "s") -> Callable:
"""Параметризованный декоратор для замера времени выполнения функции.
Args:
unit: str - единица измерения ("s" или "ms")
Returns:
Callable - декоратор
"""
def decorator(func: Callable) -> Callable:
"""Сам декоратор."""
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Any:
"""Обёртка функции с замером времени."""
# Запоминаем время начала
start_time = time.perf_counter()
# Выполняем функцию
result = func(*args, **kwargs)
# Вычисляем затраченное время
elapsed_time = time.perf_counter() - start_time
# Конвертируем в нужные единицы
if unit == "ms":
elapsed_time *= 1000
unit_str = "ms"
else: # по умолчанию секунды
unit_str = "s"
# Печатаем результат
print(f"{func.__name__} took {elapsed_time:.1f} {unit_str}")
return result
return wrapper
return decorator
# Пример использования
@timer(unit="ms")
def slow_function():
time.sleep(1)
slow_function()
# Вывод: slow_function took 1000.0 ms
@timer(unit="s")
def another_function():
time.sleep(0.5)
another_function()
# Вывод: another_function took 0.5 s
Решение 2: С валидацией параметров
import time
import functools
from typing import Callable, Any, Literal
def timer(unit: Literal["s", "ms"] = "s") -> Callable:
"""Параметризованный декоратор с валидацией.
Args:
unit: "s" или "ms"
Raises:
ValueError: если unit не из списка допустимых
"""
# Валидируем параметр
if unit not in ("s", "ms"):
raise ValueError(f"unit must be s or ms, got {unit}")
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Any:
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
# Конвертируем в нужные единицы
if unit == "ms":
elapsed *= 1000
print(f"{func.__name__} took {elapsed:.1f} {unit}")
return result
return wrapper
return decorator
Решение 3: С опциональным логированием
import time
import functools
import logging
from typing import Callable, Any, Optional
logger = logging.getLogger(__name__)
def timer(
unit: str = "s",
verbose: bool = True,
logger_func: Optional[Callable] = None
) -> Callable:
"""Продвинутый декоратор с гибкой логировкой.
Args:
unit: "s" или "ms"
verbose: печатать ли сообщение
logger_func: функция логирования (по умолчанию print)
"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Any:
start = time.perf_counter()
try:
result = func(*args, **kwargs)
except Exception as e:
# Логируем даже при ошибке
elapsed = time.perf_counter() - start
if unit == "ms":
elapsed *= 1000
if verbose:
msg = f"{func.__name__} failed after {elapsed:.1f} {unit}"
if logger_func:
logger_func(msg)
else:
print(msg)
raise
# Нормальное выполнение
elapsed = time.perf_counter() - start
if unit == "ms":
elapsed *= 1000
if verbose:
msg = f"{func.__name__} took {elapsed:.1f} {unit}"
if logger_func:
logger_func(msg)
else:
print(msg)
return result
return wrapper
return decorator
# Примеры
@timer(unit="ms")
def api_call():
time.sleep(0.1)
@timer(unit="s", logger_func=logger.info)
def database_query():
time.sleep(0.2)
api_call() # print
database_query() # logger.info
Решение 4: Класс-декоратор
Альтернативный подход с использованием класса:
import time
from typing import Any, Callable
class Timer:
"""Декоратор-класс для замера времени."""
def __init__(self, unit: str = "s"):
"""Инициализация с параметром."""
self.unit = unit
self.start_time = None
def __call__(self, func: Callable) -> Callable:
"""Делает класс вызываемым как декоратор."""
def wrapper(*args, **kwargs) -> Any:
self.start_time = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - self.start_time
if self.unit == "ms":
elapsed *= 1000
print(f"{func.__name__} took {elapsed:.1f} {self.unit}")
return result
return wrapper
# Использование
@Timer(unit="ms")
def my_function():
time.sleep(1)
my_function()
# Вывод: my_function took 1000.0 ms
Решение 5: С дополнительной статистикой
import time
import functools
from typing import Callable, Any, Dict, List
class TimerWithStats:
"""Декоратор, собирающий статистику вызовов."""
def __init__(self, unit: str = "s"):
self.unit = unit
self.stats: Dict[str, List[float]] = {}
def __call__(self, func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Any:
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
# Сохраняем статистику
if func.__name__ not in self.stats:
self.stats[func.__name__] = []
self.stats[func.__name__].append(elapsed)
# Конвертируем в нужные единицы
display_time = elapsed * 1000 if self.unit == "ms" else elapsed
print(f"{func.__name__} took {display_time:.1f} {self.unit}")
return result
return wrapper
def get_stats(self, func_name: str) -> Dict[str, float]:
"""Возвращает статистику по функции."""
times = self.stats.get(func_name, [])
if not times:
return {}
return {
"calls": len(times),
"total": sum(times),
"avg": sum(times) / len(times),
"min": min(times),
"max": max(times),
}
# Использование
timer_stats = TimerWithStats(unit="ms")
@timer_stats
def process():
time.sleep(0.1)
for _ in range(3):
process()
print(timer_stats.get_stats("process"))
# {calls: 3, total: 0.3, avg: 0.1, min: 0.099, max: 0.101}
Ключевые моменты
- functools.wraps: Сохраняет исходные метаданные функции (name, doc)
- time.perf_counter(): Лучше, чем time.time() для замера интервалов (устойчив к системным часам)
- Замыкания (closures): Вложенные функции имеют доступ к переменным родительского скопа
- Параметризация: Внешняя функция принимает параметры и возвращает декоратор
- Обработка ошибок: Декоратор должен пробросить исключение, но можно логировать его
Тесты
import unittest
import io
import sys
class TestTimer(unittest.TestCase):
def test_milliseconds(self):
"""Тест замера в миллисекундах."""
@timer(unit="ms")
def func():
time.sleep(0.1)
# Захватываем вывод
captured = io.StringIO()
sys.stdout = captured
func()
sys.stdout = sys.__stdout__
output = captured.getvalue()
assert "ms" in output
assert "func took" in output
def test_seconds(self):
"""Тест замера в секундах."""
@timer(unit="s")
def func():
time.sleep(0.05)
captured = io.StringIO()
sys.stdout = captured
func()
sys.stdout = sys.__stdout__
output = captured.getvalue()
assert output.count("s") >= 1 # Единица измерения
Что может спросить интервьюер
- Почему используется functools.wraps?
- Различие между time.time() и time.perf_counter()
- Как обработать исключения в декораторе?
- Можно ли применить несколько декораторов на одну функцию?
- Разница между функцией-декоратором и классом-декоратором
- Как собирать статистику вызовов?