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

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

В этой статье мы собираемся изучить способы, с помощью которых вы можете протестировать и базировать свой код Python. Библиотеки, которые мы рассмотрим, находятся в свободном доступе и предоставляют гибкие возможности для выполнения таких задач, как определение времени производительности, измерение потребления ресурсов и многое другое. Давайте начнем.

1. timeit

Во-первых, это утилита Python, которая существует уже некоторое время и широко популярна для выполнения быстрых тестов производительности. Давайте настроим простой тестовый скрипт и воспользуемся timeit.Timer для запуска простого теста времени:

#!/usr/bin/env python3
# test.py
import timeit
import time
def long_function():
    print('function start')
    time.sleep(5)
    print('function end')
print(timeit.Timer(long_function).timeit(number=2))

Внутри нашего long_function мы вводим некоторую задержку, используя time.sleep, чтобы имитировать некоторые длительные задачи. Затем, чтобы проверить нашу функцию, мы передаем ее timeit.Timer. Класс Timer измеряет общую скорость выполнения функции. Аргумент number указывает, сколько раз следует повторить тест. Это полезно, если у вас есть функции, время выполнения которых может даже незначительно отличаться. Повторение теста позволит лучше понять скорость, потому что у вас будет больше данных для работы.

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

function start
function end
function start
function end
10.00957689608913

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

Ознакомьтесь с подробной документацией для получения дополнительной информации:

2. line_profiler

Следующая библиотека, которую мы рассмотрим, называется line_profiler, и ее использование немного более уникально, чем другие решения. Библиотека line_profiler позволяет получить время выполнения каждой отдельной строки в файле. Это невероятно полезно, если у вас возникли проблемы с сокращением числа медленных функций или сторонних вызовов в файле большего размера. Наблюдая за временем, потраченным на каждую строку, вы можете быстро выявить проблемы, а не копаться в строке за строкой плотного кода.

Стандартное использование line_profiler сначала может показаться немного запутанным, но если вы воспользуетесь им несколько раз, это станет проще. Чтобы профилировать свой код, вам нужно добавить @profile декораторов к каждой функции. Давайте еще раз воспользуемся нашим предыдущим примером и настроим его, чтобы увидеть, как это работает:

#!/usr/bin/env python3
# test.py
import time
@profile
def long_function():
    print('function start')
    time.sleep(5)
    print('function end')
long_function()

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

kernprof -l test.py
python -m line_profiler test.py.lprof

Первая команда, приведенная выше, фактически запустит line_profiler для вашего файла и сгенерирует отдельный .lprof файл в том же каталоге. Этот .lprof файл содержит результаты, по которым можно составить отчет с помощью самого модуля во второй команде. Давайте посмотрим на вывод второй команды:

Timer unit: 1e-06 s
Total time: 5.00472 s
File: test.py
Function: long_function at line 6
Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
     6                                           @profile
     7                                           def long_function():
     8         1         15.0     15.0      0.0      print('function start')
     9         1    5004679.0 5004679.0    100.0      time.sleep(5)
    10         1         21.0     21.0      0.0      print('function end')

Каждая строка профилированной функции приводится с подробной статистикой. Поскольку мы проводим так много времени в long_function спящем режиме, это занимает почти 100% времени выполнения файла. Использование line_profiler для генерации разбивки того, на что уходит все ваше время выполнения, позволяет вам быстро определить, где вам может потребоваться сосредоточиться на рефакторинге или ускорении медленных задач.

Ознакомьтесь с документацией для получения дополнительной информации:

3. ресурс

В следующих двух библиотеках мы собираемся переключить передачи и сосредоточиться на основных ресурсных последствиях запуска кода Python. Возможность определить, сколько процессора и памяти использует ваш код, практически обязательна в современной среде. Отключение неограниченных процессов для пережевывания циклов - табу, и вы можете погрузиться в мир боли. Вот тут-то и пригодится resource. Эта библиотека позволит вам измерять использование ресурсов в вашем коде и даже устанавливать ограничения на то, какая часть конкретного ресурса может быть потреблена.

Давайте посмотрим на пример того, как проверить использование ЦП из сценария:

#!/usr/bin/env python3
# test.py
import time
from resource import getrusage, RUSAGE_SELF
def long_function():
    for i in range(10 ** 10):
        2 + 2
long_function()
print(getrusage(RUSAGE_SELF))

В этом примере мы снова изменили существующий сценарий, чтобы получить пакет resource. Чтобы увеличить нагрузку на ЦП во время long_function, мы перебираем большой диапазон чисел и заставляем ЦП выполнять некоторые вычисления. Это приведет к большей нагрузке, чем просто сон.

После завершения нашего теста мы должны увидеть следующий результат использования:

resource.struct_rusage(ru_utime=152.395004, ru_stime=0.035994, ru_maxrss=8536, ru_ixrss=0, ru_idrss=0, ru_isrss=0, ru_minflt=1092, ru_majflt=0, ru_nswap=0, ru_inblock=0, ru_oublock=0, ru_msgsnd=0, ru_msgrcv=0, ru_nsignals=0, ru_nvcsw=0, ru_nivcsw=1604)

Из этого вывода видно, что мы потратили довольно много циклов пользовательского процессора. Если вы посмотрите на цифру ru_utime (время пользователя), она показывает, что мы потратили в общей сложности 152 секунды времени. Вы можете не только измерять время процессора, но и некоторые другие показатели, которые также дают вам представление о таких вещах, как блочный ввод-вывод и использование памяти стека.

Для получения более подробной информации ознакомьтесь с документацией:

4. memory_profiler

Библиотека memory_profiler похожа на line_profiler, но ориентирована на создание статистики, непосредственно связанной с использованием памяти. Когда вы запускаете memory_profiler свой код, вы по-прежнему получаете разбивку по строкам, но с акцентом на общее и добавочное использование памяти по строкам.

Как и в line_profiler, мы собираемся использовать ту же структуру декоратора для тестирования нашего кода. Вот модифицированный пример кода:

#!/usr/bin/env python3
# test.py
@profile
def long_function():
    data = []
    for i in range(100000):
        data.append(i)
    return data
long_function()

В приведенном выше примере мы создаем тестовый список и помещаем в него большой диапазон целых чисел. Это медленно расширяет список, чтобы мы могли видеть, как использование памяти растет с течением времени. Чтобы просмотреть memory_profiler отчет, вы можете просто запустить:

python -m memory_profiler test.py

Это должно создать следующий отчет, содержащий построчную статистику памяти:

Filename: tat.py
Line #    Mem usage    Increment  Occurences   Line Contents
============================================================
     3   38.207 MiB   38.207 MiB           1   @profile
     4                                         def long_function():
     5   38.207 MiB    0.000 MiB           1       data = []
     6   41.934 MiB    2.695 MiB      100001       for i in range(100000):
     7   41.934 MiB    1.031 MiB      100000           data.append(i)
     8   41.934 MiB    0.000 MiB           1       return data

Как видите, наша функция начинается примерно с 38MB использования памяти и увеличивается до 41.9MB после заполнения нашего списка. Хотя вы можете получить информацию об использовании памяти из библиотеки resource, она не дает подробной построчной разбивки, как memory_profiler. Если вы ищете утечку памяти или имеете дело с особенно раздутым приложением, это правильный путь.

Посетите memory_profiler GitHub, чтобы узнать больше:

Спасибо за внимание! Надеюсь, вам понравилось копаться в этих великолепных библиотеках производительности Python. Напишите в Твиттере некоторые из ваших любимых способов тестирования и точной настройки кода Python!