Профилирование кода на Python с использованием timeit и cProfile
Профилирование кода на Python
Как разработчик программного обеспечения, вы, вероятно, слышали цитату «Преждевременная оптимизация – корень всех зол» – не раз – в своей карьере. В то время как оптимизация может быть не особенно полезной (или абсолютно необходимой) для маленьких проектов, профилирование часто бывает полезным.
После завершения написания модуля, хорошей практикой является профилирование вашего кода для измерения времени выполнения каждого из его разделов. Это может помочь выявить проблемы в коде и указать на возможности оптимизации для улучшения качества кода. Поэтому всегда профилируйте свой код перед оптимизацией!
Чтобы сделать первые шаги, этот руководство поможет вам начать работу с профилированием в Python – с использованием встроенных модулей timeit и cProfile. Вы узнаете, как использовать как интерфейс командной строки, так и соответствующие вызываемые функции внутри сценариев Python.
- Революционирование создания 3D-моделей с помощью MVDream
- Расшифровка бизнес-задач искусство создания аналитических решений
- 10 Математических концепций для программистов
Как профилировать код 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, науку о данных и обработку естественного языка. Она любит чтение, письмо, кодирование и кофе! В настоящее время она работает над обучением и делится своими знаниями с сообществом разработчиков, создавая учебники, руководства, мнения и многое другое.