Как замерить скорость работы 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Видно, что основное время уходит на выполнение внутреннего цикла. Такая детализация позволяет целенаправленно оптимизировать самые затратные строки.