← Назад к вопросам

Параметризованный декоратор замера времени

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}

Ключевые моменты

  1. functools.wraps: Сохраняет исходные метаданные функции (name, doc)
  2. time.perf_counter(): Лучше, чем time.time() для замера интервалов (устойчив к системным часам)
  3. Замыкания (closures): Вложенные функции имеют доступ к переменным родительского скопа
  4. Параметризация: Внешняя функция принимает параметры и возвращает декоратор
  5. Обработка ошибок: Декоратор должен пробросить исключение, но можно логировать его

Тесты

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()
  • Как обработать исключения в декораторе?
  • Можно ли применить несколько декораторов на одну функцию?
  • Разница между функцией-декоратором и классом-декоратором
  • Как собирать статистику вызовов?
Параметризованный декоратор замера времени | PrepBro