Как замерить скорость работы Python скриптов: практические приёмы

Раздел: Python -> Профилирование

Измерение времени выполнения кода позволяет выявить узкие места и оптимизировать программу. В Python существует несколько подходов к решению этой задачи. Рассмотрим основные инструменты и их особенности.

Основные методы измерения времени

Как измерить время выполнения небольшого фрагмента кода с помощью timeit?

Модуль timeit является стандартным средством для точного измерения времени выполнения небольших фрагментов кода. Он автоматически отключает сборщик мусора и выполняет указанный код заданное количество раз, возвращая общее время выполнения. Пример использования:

import timeit

code = "x = sum(range(100))"
t = timeit.timeit(code, number=100000)
print(f"Время: {t} сек")

Python время выполнения (измерение времени выполнения кода в python)

Функция timeit() принимает строку с кодом, количество повторений (number) и необязательный параметр setup для подготовительных операций. Для получения статистики по нескольким запускам используется repeat().

Типичные проблемы:

  • Если код использует внешние переменные, их нужно передать через параметр globals или определить в setup.
  • Слишком малое значение number может дать результат близкий к нулю – следует увеличить количество итераций.
  • Код с побочными эффектами (запись в файл, изменение глобальных переменных) может давать некорректные результаты – каждый запуск меняет состояние.

Цели применения: сравнение микро-оптимизаций, выбор между альтернативными реализациями, тестирование производительности отдельных конструкций.

Как измерить время выполнения с помощью модуля time?

Модуль time содержит функции time(), perf_counter() и process_time(). Для измерения интервалов лучше всего подходит perf_counter() – монотонный таймер с высоким разрешением. Пример:

import time

start = time.perf_counter()
# измеряемый код
_ = [i**2 for i in range(10**6)]
end = time.perf_counter()
print(f"Время: {end - start:.6f} сек")

Python сколько времени (измерение длительности в python)

Этот метод прост и не требует внешних библиотек. Он подходит для измерения времени выполнения целых функций или блоков кода, когда не требуется высокая точность и статистическая обработка.

Ошибки:

  • Использование time.time() для коротких операций – разрешение может быть недостаточным (например, 15 мс на Windows).
  • Однократный замер может сильно варьироваться – рекомендуется выполнять несколько запусков и усреднять.
Как профилировать код с помощью cProfile?

Модуль cProfile предназначен для детального профилирования всех вызовов функций во время выполнения. Он собирает статистику по времени, количеству вызовов, времени в каждой функции. Запуск из командной строки: python -m cProfile script.py. Внутри кода:

import cProfile

def my_func():
    # некоторый код
    pass

cProfile.run('my_func()', sort='cumtime')

Результат можно сохранить в файл и проанализировать с помощью pstats. Этот метод удобен для выявления узких мест в больших программах.

Проблемы:

  • Накладные расходы профилировщика замедляют выполнение.
  • Вывод может быть очень большим – следует фильтровать с помощью pstats.
  • cProfile не показывает время, проведённое внутри встроенных C-функций (например, sort).
Как создать декоратор для замера времени?

Декоратор – удобный способ обернуть функцию и автоматически замерять её время выполнения. Пример:

import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} выполнена за {elapsed:.6f} сек")
        return result
    return wrapper

@timer
def compute():
    return sum(range(10**6))

compute()

Такой декоратор можно параметризовать (логгировать в файл, подавлять вывод и т.д.). Подходит для быстрой оценки времени отдельных функций без изменения их кода.

Ошибки:

  • Отсутствие @wraps приводит к потере метаданных функции (имя, документация).
  • Декоратор не работает напрямую с асинхронными функциями – нужен asyncio-aware вариант.

Как использовать line_profiler для построчного анализа?

Line_profiler – сторонняя библиотека, позволяющая измерять время выполнения каждой строки кода. Установка: pip install line_profiler. Использование:

@profile
def my_function():
    a = [i**2 for i in range(1000)]
    b = sum(a)
    return b

my_function()

Затем запуск: kernprof -l -v script.py. Вывод показывает время для каждой строки. Это даёт детальное понимание, какие строки кода самые затратные.

Проблемы:

  • Необходимость установки дополнительного пакета.
  • Замедление выполнения (каждая строка инструментируется).
  • Не работает для встроенных функций или модулей без декорирования.

Пример 1: timeit.repeat с оценкой разброса

Сравнение двух способов вычисления квадратных корней с помощью repeat для получения стабильной оценки.

Пример
import timeit

setup = "from math import sqrt"
stmt1 = "[sqrt(i) for i in range(1000)]"
stmt2 = "list(map(sqrt, range(1000)))"

timings1 = timeit.repeat(stmt1, setup, number=10000, repeat=7)
timings2 = timeit.repeat(stmt2, setup, number=10000, repeat=7)

print("Генератор списка:")
print(f"  min: {min(timings1):.4f}, max: {max(timings1):.4f}, mean: {sum(timings1)/len(timings1):.4f}")
print("map с list:")
print(f"  min: {min(timings2):.4f}, max: {max(timings2):.4f}, mean: {sum(timings2)/len(timings2):.4f}")
Генератор списка:
  min: 0.2345, max: 0.2456, mean: 0.2398
map с list:
  min: 0.1890, max: 0.1987, mean: 0.1932

Минимальное значение обычно рассматривается как наиболее стабильная оценка, так как оно меньше подвержено влиянию фоновых процессов. Среднее также полезно для общей картины.

Пример 2: cProfile и pstats с сортировкой по времени

Профилирование рекурсивной функции вычисления факториала с сохранением результатов в файл.

Пример
import cProfile, pstats

def factorial(n):
    if n == 0:
        return 1
    return n * factorial(n-1)

cProfile.run('factorial(500)', 'factorial_stats.prof')
p = pstats.Stats('factorial_stats.prof')
p.sort_stats('time').print_stats(15)
Thu Jul 18 12:34:56 2024    factorial_stats.prof

         501 function calls (1 primitive call) in 0.000 seconds

   Ordered by: internal time
   List reduced from 3 to 3 due to restriction <15>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      500    0.000    0.000    0.000    0.000 script.py:5(factorial)
        1    0.000    0.000    0.000    0.000 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 <string>:1(<module>)

Отчёт показывает количество вызовов и время, проведённое внутри каждой функции. Для больших программ фильтрация по cumtime (накопленное время) помогает найти самые затратные цепочки вызовов.

Пример 3: Декоратор с записью в файл и возможностью отключения

Расширенный декоратор, который может логировать время в указанный файл и отключаться.

Пример
import time
from functools import wraps

def timer_with_log(log_file=None, enabled=True):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            if not enabled:
                return func(*args, **kwargs)
            start = time.perf_counter()
            result = func(*args, **kwargs)
            elapsed = time.perf_counter() - start
            msg = f"{func.__name__} заняла {elapsed:.6f} сек"
            print(msg)
            if log_file:
                with open(log_file, 'a') as f:
                    f.write(msg + '\n')
            return result
        return wrapper
    return decorator

@timer_with_log(log_file='timing.log')
def heavy_operation(n):
    return sum(i**2 for i in range(n))

heavy_operation(100000)
heavy_operation заняла 0.012345 сек

Такой декоратор гибок: можно временно отключить замер, изменить целевой файл или даже подавить вывод на консоль.

Пример 4: line_profiler для циклической обработки данных

Демонстрация построчного профилирования функции с двойным циклом.

Пример
@profile
def process_data():
    total = 0
    for i in range(1000):
        for j in range(1000):
            total += (i * j) % 255
    return total

if __name__ == '__main__':
    process_data()

Запуск: kernprof -l -v script.py. Результат:

Wrote profile results to script.py.lprof
Timer unit: 1e-07 s

Total time: 1.23456 s
File: script.py
Function: process_data at line 1

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     1                                           @profile
     2                                           def process_data():
     3         1          2.0      2.0      0.0      total = 0
     4      1001      50000.0     49.9      4.0      for i in range(1000):
     5   1000000   1200000.0      1.2     97.2          for j in range(1000):
     6   1000000   100000.0      0.1      8.1              total += (i * j) % 255
     7         1          1.0      1.0      0.0      return total

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

Измерение времени выполнения кода в Python - comments

En
Python время выполнения (python)