Три простых способа улучшить производительность кода Python

1. Бенчмарк, бенчмарк и еще раз бенчмарк

«Что измеряемо, то управляемо».

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

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

 $ pip3 install line_profiler

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

# название файла: test.py

@profile
def sum_of_lists(ls):
    '''считает сумму значений переданного списка списков'''

    s = 0
    for l in ls:
        for val in l:
            s += val

    return s


# создать список списков целых чисел
smallrange = list(range(10000))
inlist = [smallrange, smallrange, smallrange, smallrange]

# получить сумму
list_sum = sum_of_lists(inlist)

print(list_sum)

Во время исполнения программы функция sum_of_lists при вызове будет профилирована. Обратите внимание на декоратор @profile над определением функции.

Чтобы запустить бенчмарк, введите:

$ python3 -m line_profiler test.py

В итоге получим:

Вывод измерителя скорости функции line_profiler

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

Имейте в виду, что эта библиотека для измерения сама потребляет значительные ресурсы во время использования. Но она замечательно подходит для выявления слабых мест в программе с последующей заменой их на что-нибудь более эффективное.

Чтобы запустить line_profiler из Jupyter Notebook, попробуйте магическую команду %%lprun.

2. По возможности избегайте циклов

Во многих случаях значительно улучшить производительность в Python позволяет применение вместо циклов операций map, list comprehensions или numpy.vectorize (обычно самая быстрая). Реализация этих операций уже тщательно оптимизирована внутри. Давайте изменим наш предыдущий пример, заменив вложенные циклы на map и sum.

# название файла: test_map.py

def sum_of_lists_map(ls):
    '''считает сумму значений переданного списка списков'''

    return(sum(list(map(sum, ls))))


# создать список списков целых чисел
smallrange = list(range(10000))
inlist = [smallrange, smallrange, smallrange, smallrange]

# получить сумму
list_sum = sum_of_lists_map(inlist)

print(list_sum)

Посмотрим, насколько резво работает новая версия с картой по сравнению с изначальной. Измерим их скорость 1000 раз.

Версия с map более чем в 6 раз быстрее!

Производительность оптимизированной версии с map вместо циклов

3. Компилируйте ваши модули Python с помощью Cython

Если совсем нет желания редактировать проект, но хочется хоть какого-нибудь улучшения производительности без лишних усилий, ваш лучший друг – Cython.

Хотя Cython не является транспайлером общего назначения с Python в С, он позволяет скомпилировать модули Python в файлы общих объектов (.so). Их можно загрузить в ваш основной скрипт на Python.

Для этого потребуется установить на машине как собственно Cython, так и компилятор С:

$ pip3 install cython

Если вы работаете на Debian системе, загрузите GCC следующим образом:

$ sudo apt install gcc

Давайте разделим изначальный код примера на два файла с названиями test_cython.py и test_module.pyx:

# название файла: test_module.pyx

def sum_of_lists(ls):
    '''считает сумму значений переданного списка списков'''

    s = 0
    for l in ls:
        for val in l:
            s += val

    return s

Наш главный файл должен импортировать эту функцию с файла test_module.pyx:

# название файла: test_cython.py

from test_module import *

# создать список списков целых чисел
smallrange = list(range(10000))
inlist = [smallrange, smallrange, smallrange, smallrange]

# получить сумму
list_sum = sum_of_lists(inlist)

print(list_sum)

Теперь напишем скрипт setup.py для компиляции нашего модуля при помощи Cython:

# название файла: setup.py

from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules=cythonize("test_module.pyx")
)

Наконец пришло время скомпилировать наш модуль:

$ python3 setup.py build_ext --inplace

Теперь сравним эффективность этой версии с оригинальной, произведя, снова-таки, тысячу измерений.

В этом случае Cython улучшил производительность нашей программы почти вдвое по сравнению с первым вариантом. Но этот показатель будет меняться в зависимости от того, какого рода код вы пытаетесь оптимизировать.

Производительность версии программы, скомпилированной при помощи Cython

Если вам нужно будет воспользоваться преимуществами Cython в Jupyter Notebook, то там доступна волшебная команда %%Cython. С ней вы скомпилируйте свои функции без особых усилий.

Итоги

Мы рассмотрели 3 простых в реализации способа увеличить производительность кода Python. Если желаете узнать больше о line_profiler и применении Cython в Jupyter, предлагаем ознакомится с магическими методами %%lprun и %%cython.