Как замерить время работы программы на Python: от простых таймеров до продвинутого профилирования

Раздел: 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 – декораторы с логированием. Всегда учитывайте влияние самого измерительного кода на результаты.

Измерение длительности в Python - comments

En
Python сколько времени (python)