Профилирование кода на Python с использованием timeit и cProfile

Профилирование кода на Python

 

Как разработчик программного обеспечения, вы, вероятно, слышали цитату «Преждевременная оптимизация – корень всех зол» – не раз – в своей карьере. В то время как оптимизация может быть не особенно полезной (или абсолютно необходимой) для маленьких проектов, профилирование часто бывает полезным.

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

Чтобы сделать первые шаги, этот руководство поможет вам начать работу с профилированием в Python – с использованием встроенных модулей timeit и cProfile. Вы узнаете, как использовать как интерфейс командной строки, так и соответствующие вызываемые функции внутри сценариев Python.

 

Как профилировать код Python, используя timeit

 

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

Давайте рассмотрим простой пример – разворачивание списка Python. Мы измерим время выполнения получения развернутой копии списка с помощью:

  • функции reversed(),
  • среза списка.
>>> nums=[6,9,2,3,7]
>>> list(reversed(nums))
[7, 3, 2, 9, 6]
>>> nums[::-1]
[7, 3, 2, 9, 6]

 

 

Запуск timeit из командной строки

 

Вы можете запустить timeit из командной строки с помощью следующего синтаксиса:

$ python -m timeit -s 'setup-code' -n 'number' -r 'repeat' 'stmt'

 

Вы должны предоставить выражение stmt, время выполнения которого должно быть измерено.

Вы можете указать код setup, когда это необходимо, используя короткую опцию -s или длинную опцию –setup. Код setup будет выполнен только один раз.

Количество выполнений выражения: короткая опция -n или длинная опция –number является необязательной. И количество повторений этого цикла: короткая опция -r или длинная опция –repeat также является необязательной.

Давайте посмотрим, как это работает на нашем примере:

Здесь создание списка является кодом setup, а разворачивание списка – выражением, время выполнения которого нужно измерить:

$ python -m timeit -s 'nums=[6,9,2,3,7]' 'list(reversed(nums))'
500000 loops, best of 5: 695 nsec per loop

 

Когда вы не указываете значения для repeat, используется значение по умолчанию – 5. И когда вы не указываете number, код выполняется столько раз, сколько необходимо, чтобы достичь общего времени, равного по крайней мере 0,2 секунды.

В этом примере явно задается количество выполнений выражения:

$ python -m timeit -s 'nums=[6,9,2,3,7]' -n 100Bu000 'list(reversed(nums))'
100000 loops, best of 5: 540 nsec per loop

 

Значение по умолчанию для repeat – 5, но мы можем задать любое подходящее значение:

$ python3 -m timeit -s 'nums=[6,9,2,3,7]' -r 3 'list(reversed(nums))'
500000 loops, best of 3: 663 nsec per loop

 

Давайте также замерим время выполнения подхода с использованием среза списка:

$ python3 -m timeit -s 'nums=[6,9,2,3,7]' 'nums[::-1]'
1000000 loops, best of 5: 142 nsec per loop

 

Подход с использованием срезов списка, кажется, более быстрый (все примеры приведены на Python 3.10 на Ubuntu 22.04).

 

Запуск timeit в скрипте Python

 

Вот эквивалент запуска timeit внутри скрипта Python:

import timeit

setup = 'nums=[9,2,3,7,6]'
number = 100000
stmt1 = 'list(reversed(nums))'
stmt2 = 'nums[::-1]'

t1 =  timeit.timeit(setup=setup,stmt=stmt1,number=number)
t2 = timeit.timeit(setup=setup,stmt=stmt2,number=number)

print(f"Использование функции reversed(): {t1}")
print(f"Использование среза списка: {t2}")

 

Функция timeit() возвращает время выполнения stmt для number раз. Обратите внимание, что мы можем явно указать количество запусков или использовать значение по умолчанию – 1000000.

Вывод >>
Использование функции reversed(): 0.08982690000000002
Использование среза списка: 0.015550800000000004

 

Этот код выполняет оператор – без повторного вызова функции таймера – указанное количество number раз и возвращает время выполнения. Также довольно распространено использование time.repeat() и взятие минимального времени, как показано ниже:

import timeit

setup = 'nums=[9,2,3,7,6]'
number = 100000
stmt1 = 'list(reversed(nums))'
stmt2 = 'nums[::-1]'

t1 =  min(timeit.repeat(setup=setup,stmt=stmt1,number=number))
t2 = min(timeit.repeat(setup=setup,stmt=stmt2,number=number))

print(f"Использование функции reversed(): {t1}")
print(f"Использование среза списка: {t2}")

 

Это повторяет процесс выполнения кода number раз repeat количество раз и возвращает минимальное время выполнения. Здесь мы выполняем 5 повторений по 100000 раз каждое.

Вывод >>
Использование функции reversed(): 0.055375300000000016
Использование среза списка: 0.015101400000000043

 

Как профилировать скрипт Python с использованием cProfile

 

Мы видели, как timeit может использоваться для измерения времени выполнения небольших фрагментов кода. Однако на практике более полезным является профилирование всего скрипта Python.

Это дает нам времена выполнения всех функций и вызовов методов, включая встроенные функции и методы. Таким образом, мы можем получить лучшее представление о вызовах более затратных функций и идентифицировать возможности для оптимизации. Например: может быть вызов API, который работает слишком медленно. Или функция может содержать цикл, который можно заменить более питоническим выражением-генератором.

Давайте узнаем, как профилировать скрипты Python с использованием модуля cProfile (также является частью стандартной библиотеки Python).

Рассмотрим следующий скрипт Python:

# main.py
import time

def func(num):
    for i in range(num):
        print(i)

def another_func(num):
    time.sleep(num)
    print(f"Спал {num} секунд")

def useful_func(nums, target):
    if target in nums:
        return nums.index(target)

if __name__ == "__main__":
    func(1000)
    another_func(20)
    useful_func([2, 8, 12, 4], 12)

 

Здесь у нас три функции:

  • func(), которая проходит по диапазону чисел и выводит их.
  • another_func(), которая содержит вызов функции sleep().
  • useful_func(), которая возвращает индекс целевого числа в списке (если цель присутствует в списке).

Перечисленные выше функции будут вызываться каждый раз при запуске скрипта main.py.

 

Запуск cProfile из командной строки

 

Запустите cProfile из командной строки с помощью:

python3 -m имя-файла.py

 

Здесь мы назвали файл main.py:

python3 -m main.py

 

При выполнении этой команды вы получите следующий вывод:

  Output >>
  0
  ...
  999
  Slept for 20 seconds

 

И следующий профиль:

   

Здесь ncalls относится к количеству вызовов функции, а percall относится к времени на один вызов функции. Если значение ncalls больше единицы, то percall – это среднее время по всем вызовам.

Время выполнения скрипта определяется функцией another_func, которая использует встроенный вызов функции sleep (засыпает на 20 секунд). Мы видим, что вызовы функции print также достаточно дорогостоящие. 

 

Использование cProfile в Python-скрипте

 

Хотя запуск cProfile из командной строки работает хорошо, вы также можете добавить функциональность профилирования в Python-скрипт. Вы можете использовать cProfile в сочетании с модулем pstats для профилирования и получения статистики.

Как bewt практика для лучшей обработки установки и разрушения ресурсов, используйте оператор with и создайте объект профиля, который используется в качестве менеджера контекста:

# main.py
import pstats
import time
import cProfile

def func(num):
    for i in range(num):
        print(i)

def another_func(num):
    time.sleep(num)
    print(f"Slept for {num} seconds")

def useful_func(nums, target):
    if target in nums:
        return nums.index(target)


if __name__ == "__main__":
    with cProfile.Profile() as profile:
        func(1000)
        another_func(20)
        useful_func([2, 8, 12, 4], 12)
    profile_result = pstats.Stats(profile)
    profile_result.print_stats()

 

Давайте ближе рассмотрим сгенерированный профиль:

   

При профилировании большого скрипта будет полезно отсортировать результаты по времени выполнения. Для этого вы можете вызвать sort_stats на объекте профиля и отсортировать по времени выполнения: 

...
if __name__ == "__main__":
    with cProfile.Profile() as profile:
        func(1000)
        another_func(20)
        useful_func([2, 8, 12, 4], 12)
    profile_result = pstats.Stats(profile)
    profile_result.sort_stats(pstats.SortKey.TIME)
    profile_result.print_stats()

 

Теперь при запуске скрипта вы должны увидеть результаты, отсортированные по времени:

 

 

Заключение

 

Надеюсь, этот руководство поможет вам начать профилирование в Python. Всегда помните, что оптимизации не должны идти в ущерб читабельности. Если вы заинтересованы в изучении других профилировщиков, включая сторонние пакеты Python, ознакомьтесь с этой статьей о профилировщиках Python.     Bala Priya C – разработчик и технический писатель из Индии. Ей нравится работа в области математики, программирования, науки о данных и создания контента. Ее интересы и экспертиза включают DevOps, науку о данных и обработку естественного языка. Она любит чтение, письмо, кодирование и кофе! В настоящее время она работает над обучением и делится своими знаниями с сообществом разработчиков, создавая учебники, руководства, мнения и многое другое.