Измеряем время работы Python скрипта: инструменты и практика
Основные методы измерения времени выполнения
Измерение времени работы программы является ключевым этапом оптимизации производительности. В Python существует несколько подходов, каждый со своими особенностями и областью применения. Рассмотрим основные варианты, начиная с наиболее точного и рекомендуемого.
Как точно измерить время выполнения небольшого фрагмента кода с помощью модуля timeit?
Модуль timeit предназначен для точного замера времени выполнения коротких операций. Он автоматически отключает сборщик мусора, запускает код многократно и выбирает наилучшее (наименьшее) значение, минимизируя влияние внешних факторов.
import timeit
code = '''
result = [i**2 for i in range(1000)]
'''
t = timeit.timeit(code, number=1000)
print(f"Время выполнения: {t} секунд")время работы программы python (время выполнения программы на python)
Время выполнения: 0.0765432 секунд
Python быстрые списки (оптимизация работы со списками в python)
Пояснение: параметр number задает количество повторений. Результат показывает общее время выполнения всех повторений. Для получения среднего времени нужно разделить на number.
Типичные ошибки:
- Измерение очень быстрых операций без достаточного числа повторений (результат будет искажен шумом).
- Использование timeit для кода, содержащего импорты внутри измеряемого блока (можно вынести настройку в setup параметр).
Решение: для очень быстрых операций увеличивайте number до тысяч или миллионов. Импорты и подготовительные действия передавайте через аргумент setup.
Как измерить время выполнения с помощью time.time и time.perf_counter?
Функции time.time() и time.perf_counter() возвращают текущее время в секундах. Разница между двумя последовательными вызовами дает интервал.
import time
start = time.perf_counter()
# некоторый код
for _ in range(1000000):
pass
end = time.perf_counter()
print(f"Время: {end - start} сек")
скорость выполнения программы python (скорость выполнения программы на python)
Время: 0.0324567 сек
perf_counter использует высокоточный счетчик (наносекунды), в то время как time.time может иметь точность около миллисекунды. Для большинства задач предпочтительнее perf_counter.
Проблемы:
- На однопроцессорных системах perf_counter может быть чувствителен к переключениям контекста.
- Если код слишком быстрый, разница может быть близка к нулю из-за дискретизации.
Решение: выполнять код многократно в цикле, суммировать время и делить на количество итераций.
Как использовать time.process_time для измерения процессорного времени?
Функция time.process_time() возвращает время, затраченное процессором на выполнение текущего процесса (без учета ожидания ввода-вывода).
import time
def heavy_task():
total = 0
for i in range(1, 100000):
total += i ** 0.5
return total
start = time.process_time()
result = heavy_task()
end = time.process_time()
print(f"Процессорное время: {end - start} сек")Процессорное время: 0.0421 сек
Этот метод полезен, когда нужно исключить влияние фоновой нагрузки системы.
Ошибка:
Не учитывает время ожидания (например, сетевых запросов). Для задач, где важна общая задержка, используйте perf_counter.
Как создать декоратор для автоматического замера времени функций?
Декоратор позволяет прозрачно добавить логирование времени выполнения к любой функции.
import time
from functools import wraps
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} выполнена за {end - start:.6f} сек")
return result
return wrapper
@timer
def compute_something(n):
return sum(i * i for i in range(n))
print(compute_something(10000))compute_something выполнена за 0.004532 сек 333283335000
Такой подход удобен при профилировании нескольких функций без изменения их тела.
Проблемы:
- Декоратор добавляет накладные расходы на каждый вызов.
- При многопоточности может потребоваться потокобезопасный замер.
Решение: использовать threading.Lock или time.perf_counter_ns для большей точности.
Как использовать cProfile для профилирования всего приложения?
Модуль cProfile собирает статистику времени выполнения всех вызванных функций, что помогает найти узкие места.
import cProfile
def main():
total = 0
for i in range(1000):
total += sum_of_squares(i)
return total
def sum_of_squares(n):
return sum(x*x for x in range(n))
cProfile.run('main()', sort='cumtime') 1004 function calls in 0.051 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.051 0.051 <string>:1(<module>)
1 0.000 0.000 0.051 0.051 main.py:11(main)
1000 0.019 0.000 0.050 0.000 main.py:15(sum_of_squares)
1000 0.031 0.000 0.031 0.000 main.py:16(<genexpr>)Пояснение: столбец cumtime показывает суммарное время функции со всеми вложенными вызовами.
Распространенная ошибка:
Профилирование кода, который работает слишком быстро (менее 0.01 сек) - результаты могут быть неточными. Рекомендуется увеличивать нагрузку или использовать цикл.
Как измерить время с помощью datetime.datetime.now?
Хотя datetime не предназначен для высокоточных замеров, его можно использовать для грубой оценки.
from datetime import datetime
start = datetime.now()
# долгая операция
for _ in range(1000000):
_ = 1+1
end = datetime.now()
print(f"Время: {end - start}")Время: 0:00:00.034567
Этот способ удобен при необходимости быстро оценить время работы в днях/часах.
Недостаток:
Точность ~ миллисекунды, отсутствие возможности многократных повторений. Не подойдет для микрооптимизаций.
Расширенные примеры измерения времени выполнения
Сравнение list comprehension и обычного цикла с помощью timeit
import timeit
setup = '''
import random
numbers = [random.random() for _ in range(10000)]
'''
code_loop = '''
result = []
for x in numbers:
result.append(x ** 2)
'''
code_comp = '''
result = [x ** 2 for x in numbers]
'''
t1 = timeit.timeit(code_loop, setup=setup, number=1000)
t2 = timeit.timeit(code_comp, setup=setup, number=1000)
print(f"Цикл: {t1:.4f} сек")
print(f"Генератор: {t2:.4f} сек")Цикл: 1.2345 сек Генератор: 1.0123 сек
Пояснение: setup выполняется один раз для подготовки данных. timeit выполняет каждый кусок кода 1000 раз. Результат показывает общее время, что позволяет сравнивать производительность.
Замер времени выполнения рекурсивной функции с защитой от глубины рекурсии
import timeit
import sys
sys.setrecursionlimit(10000)
def factorial(n):
return 1 if n <= 1 else n * factorial(n-1)
code = '''
factorial(500)
'''
t = timeit.timeit(code, globals=globals(), number=100)
print(f"Среднее время на вызов: {t/100:.6f} сек")Среднее время на вызов: 0.000152 сек
Параметр globals=globals() позволяет timeit видеть определенные в текущем модуле функции. Без него вызов factorial будет недоступен.
Профилирование с помощью cProfile и pstats для сохранения результатов
import cProfile, pstats
def slow_function():
total = 0
for i in range(1000):
for j in range(1000):
total += i * j
return total
profiler = cProfile.Profile()
profiler.enable()
result = slow_function()
profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats('cumtime')
stats.print_stats(10) # вывести 10 самых затратных 1003 function calls in 0.456 seconds
Ordered by: cumulative time
List reduced from 3 to 10 due to restriction
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.456 0.456 <string>:1(<module>)
1 0.000 0.000 0.456 0.456 example.py:5(slow_function)
1 0.456 0.456 0.456 0.456 {range}Метод print_stats с ограничением строк удобен для быстрого анализа. Результаты можно сохранить в файл.
Использование декоратора с возможностью накопления статистики
import time
from functools import wraps
from collections import defaultdict
stats = defaultdict(float)
def timer_with_stats(name):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
stats[name] += end - start
return result
return wrapper
return decorator
@timer_with_stats('expensive')
def process_data(data):
# имитация обработки
return sum(x * x for x in data)
for _ in range(10):
process_data(range(10000))
print("Накопленное время:", dict(stats))Накопленное время: {'expensive': 0.345678}Такой декоратор позволяет собирать общее время по именованным операциям, что полезно для мониторинга.
Измерение времени с высокой точностью с помощью time.perf_counter_ns
import time
def nano_bench():
start = time.perf_counter_ns()
# очень быстрая операция
_ = 1 + 1
end = time.perf_counter_ns()
return end - start
print(f"Время в наносекундах: {nano_bench()}")Время в наносекундах: 114
Использование perf_counter_ns (Python 3.7+) позволяет избежать ошибок округления при пересчете секунд.