Как замерить время работы программы на Python: от простых таймеров до продвинутого профилирования
Измерение длительности выполнения кода в Python
Как измерить время работы функции с максимальной точностью и минимальными накладными расходами?
Наиболее эффективным и рекомендуемым инструментом для измерения интервалов времени внутри программы является функция time.perf_counter() из стандартного модуля time. Она предоставляет монотонные часы с наивысшим доступным разрешением (обычно микросекунды или наносекунды) и не подвержена корректировкам системного времени (например, при синхронизации NTP).
import time
def heavy_computation():
total = 0
for i in range(10**6):
total += i ** 2
return total
start = time.perf_counter()
result = heavy_computation()
end = time.perf_counter()
elapsed = end - start
print(f"Функция выполнилась за {elapsed:.6f} секунд")Python время выполнения (измерение времени выполнения кода в python)
Функция выполнилась за 0.089452 секунд
Python сколько времени (измерение длительности в python)
Типичные ошибки и проблемы:
- Использование
time.time()для коротких интервалов может давать неточности из-за низкого разрешения (около 16 мс на Windows) и зависимости от системных часов. - Не учитывается время, затраченное на сборку мусора или планирование потока – для очень коротких операций (<1 мс) рекомендуется выполнять замеры в цикле и усреднять.
- Забывают вычитать накладные расходы на сам вызов
perf_counter()– при замере микроскопических фрагментов следует использоватьtimeit.
Как измерить время выполнения с помощью модуля time?
Самый простой вариант – time.monotonic() (монотонные часы без высокого разрешения) или time.process_time() (время только текущего процесса, не включает ожидание).
import time
t0 = time.monotonic()
# ... код ...
t1 = time.monotonic()
print(f"Прошло {t1 - t0:.3f} сек")
Когда использовать:
monotonic()– для длительных операций, где важна монотонность.process_time()– для измерения только CPU-времени, без ожидания ввода-вывода.
Как замерить выполнение небольшого фрагмента кода без написания таймера вручную?
Для коротких кусков кода удобен модуль timeit, который автоматически отключает сборщик мусора и выполняет код много раз для усреднения.
import timeit
code = '''
sum(i**2 for i in range(1000))
'''
# Один запуск
single = timeit.timeit(code, number=1)
print(f"Один запуск: {single:.6f} сек")
# Усреднённое время
avg = timeit.timeit(code, number=1000) / 1000
print(f"Среднее по 1000 запускам: {avg:.9f} сек")
Один запуск: 0.000032 сек Среднее по 1000 запускам: 0.000031987 сек
Частые затруднения:
- Передача строки кода или функции – второй вариант предпочтительнее для читаемости:
timeit.timeit(lambda: sum(i**2 for i in range(1000))). - Параметр
numberвыбирается так, чтобы общее время было не менее нескольких секунд – так уменьшается погрешность.
Как измерить время работы в виде декоратора для многократного использования?
Декоратор-таймер удобен для профилирования отдельных функций без изменения их логики.
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:.4f} сек")
return result
return wrapper
@timer
def slow_add(a, b):
time.sleep(0.1)
return a + b
slow_add(3, 4)
slow_add заняла 0.1001 сек
Расширение: декоратор с накоплением статистики (среднее, количество вызовов) можно реализовать через атрибуты функции.
Как замерить время с помощью контекстного менеджера?
Контекстный менеджер позволяет оборачивать любой блок кода в with и автоматически выводить время.
import time
class Timer:
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, *args):
self.end = time.perf_counter()
elapsed = self.end - self.start
print(f"Блок выполнен за {elapsed:.4f} сек")
with Timer():
sum(range(10**7))
Блок выполнен за 0.2543 сек
Важно:
Контекстный менеджер не даёт доступа к результату замера вне блока, если не сохранить его явно (например, в атрибут экземпляра).
Как измерить время выполнения целого скрипта из командной строки?
Python предоставляет встроенные утилиты для профилирования:
python -m timeit "код"– замер времени короткого выражения.python -m cProfile script.py– вывод статистики по всем вызовам функций.
# В терминале:
# python -m timeit "'-'.join(str(n) for n in range(100))"
# Результат: 100 loops, best of 5: 9.82 usec per loop
# Профилирование:
# python -m cProfile -s cumulative my_script.py
Как получить подробную статистику по каждой строке кода (line_profiler)?
Сторонний модуль line_profiler (устанавливается через pip) позволяет профилировать построчно, что помогает найти узкие места в алгоритме.
# pip install line_profiler
@profile
def my_func():
a = [i**2 for i in range(100000)]
b = sum(a)
return b
my_func()
# Запуск: kernprof -l -v script.py
Line # Hits Time Per Hit % Time Line Contents
======================================================
1 @profile
2 def my_func():
3 1 12450.0 12450.0 100.0 a = [i**2 for i in range(100000)]
4 1 0.0 0.0 0.0 b = sum(a)
5 1 0.0 0.0 0.0 return b
Примечание:
Декоратор @profile импортируется автоматически при запуске через kernprof. В обычном коде его нет – профилирование происходит только при запуске с этим инструментом.
Расширенные примеры измерения длительности
Дополнительные сценарии, которые помогут глубже понять поведение таймеров и избежать распространённых ловушек.
Измерение времени с усреднением для высокоточной оценки
Для операций, занимающих менее 0.1 секунды, однократный замер может быть неточным из-за флуктуаций ядра ОС. Рекомендуется выполнять код многократно и вычислять среднее и стандартное отклонение.
import time
import statistics
def measure(func, iterations=100):
times = []
for _ in range(iterations):
t0 = time.perf_counter()
func()
t1 = time.perf_counter()
times.append(t1 - t0)
mean = statistics.mean(times)
stdev = statistics.stdev(times) if len(times) > 1 else 0
return mean, stdev
def light_op():
return sum(range(1000))
mean, std = measure(light_op, iterations=500)
print(f"Среднее: {mean*1e6:.3f} мкс, стд. откл.: {std*1e6:.3f} мкс")
Среднее: 12.456 мкс, стд. откл.: 1.234 мкс
Сравнение производительности двух алгоритмов с помощью timeit и визуализации результатов
Модуль timeit можно использовать для сравнения нескольких подходов, а затем построить простой график (например, через matplotlib).
import timeit
import numpy as np
setup = '''
import random
lst = [random.random() for _ in range(10000)]
'''
code1 = 'sorted(lst)'
code2 = 'lst.sort()'
t1 = timeit.timeit(code1, setup, number=1000)
t2 = timeit.timeit(code2, setup, number=1000)
print(f"sorted(): {t1:.3f} сек")
print(f"list.sort(): {t2:.3f} сек")
print(f"sorted() медленнее в {t1/t2:.2f} раз")
sorted(): 1.234 сек list.sort(): 1.121 сек sorted() медленнее в 1.10 раз
Измерение времени работы асинхронных функций (async/await)
Стандартные таймеры работают и в асинхронном коде, но для точности стоит использовать loop.time() из asyncio (монотонные часы самого цикла событий).
import asyncio
import time
async def async_task(n):
await asyncio.sleep(n)
return n
async def main():
loop = asyncio.get_running_loop()
start = loop.time()
await async_task(0.05)
end = loop.time()
print(f"Асинхронная задача: {end - start:.4f} сек")
# Можно комбинировать с обычным time.perf_counter()
start2 = time.perf_counter()
await async_task(0.05)
end2 = time.perf_counter()
print(f"Через perf_counter: {end2 - start2:.4f} сек")
asyncio.run(main())
Асинхронная задача: 0.0500 сек Через perf_counter: 0.0501 сек
Измерение времени с учётом только процессорного времени (time.thread_time)
Для многопоточных приложений или при измерении времени, проведённого только в потоке, используйте time.thread_time() (доступно с Python 3.7).
import time
def busy_wait(duration):
t0 = time.thread_time()
while time.thread_time() - t0 < duration:
pass
return time.thread_time() - t0
cpu_time = busy_wait(0.1)
wall_time = time.perf_counter() - time.perf_counter() # для демонстрации
print(f"Затрачено процессорного времени: {cpu_time:.4f} сек")
Затрачено процессорного времени: 0.1001 сек
Профилирование памяти с помощью memory_profiler (дополнительно)
Хотя статья посвящена длительности, в профилировании часто требуется одновременно смотреть память. Модуль memory_profiler позволяет замерять потребление памяти построчно.
# pip install memory_profiler
from memory_profiler import profile
@profile
def mem_hungry():
a = [i for i in range(100000)]
b = [i * 2 for i in a]
del a
return b
if __name__ == '__main__':
mem_hungry()
Line # Mem usage Increment Line Contents
================================================
3 45.2 MiB 45.2 MiB @profile
4 def mem_hungry():
5 49.4 MiB 4.2 MiB a = [i for i in range(100000)]
6 52.5 MiB 3.1 MiB b = [i * 2 for i in a]
7 52.5 MiB 0.0 MiB del a
8 52.5 MiB 0.0 MiB return b
Сохранение результатов профилирования в файл для последующего анализа
Модуль cProfile умеет сохранять статистику в бинарный файл, который затем можно проанализировать графически с помощью snakeviz.
import cProfile
import pstats
def outer():
for _ in range(1000):
inner()
def inner():
sum(range(100))
if __name__ == '__main__':
cProfile.run('outer()', 'profile_output.prof')
# Анализ через pstats
p = pstats.Stats('profile_output.prof')
p.sort_stats('cumulative').print_stats(10)
# Для графического просмотра: snakeviz profile_output.prof
1003 function calls in 0.003 seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.003 0.003 <string>:1(<module>)
1 0.000 0.000 0.003 0.003 <__main__>:3(outer)
1000 0.003 0.000 0.003 0.000 <__main__>:7(inner)
1 0.000 0.000 0.000 0.000 {built-in method builtins.print}
Заключение по расширенным примерам:
Выбор инструмента зависит от контекста: для быстрых микро-замеров используйте timeit, для детального профилирования всего приложения – cProfile/line_profiler, для мониторинга в production – декораторы с логированием. Всегда учитывайте влияние самого измерительного кода на результаты.