Python ценят за простоту, гибкость и читаемость, но критикуют за невысокую производительность. Эта критика не всегда обоснована: есть несколько эффективных способов значительно повысить скорость Python-приложений, предназначенных для выполнения сложных вычислений и обработки больших объемов данных.
Упаковка переменных
Упаковка переменных — это процесс минимизации использования памяти за счет объединения нескольких элементов данных в одну структуру. Эта техника особенно важна в сценариях, где время доступа к памяти сильно влияет на производительность, например, при обработке больших объемов связанных данных: упаковка позволяет значительно ускорить процесс с помощью эффективного использования кэша процессора.
В Python для упаковки данных в компактный бинарный формат используется модуль struct:
В приложениях, для которых критически важна производительность, необходимо хранить часто используемые данные в памяти и минимизировать операции чтения/записи на диск, чтобы обеспечить высокую скорость извлечения информации. Модуль mmap позволяет работать с файлами на диске, как если бы они находились в оперативной памяти:
Результат — вывод каждой 100-й и каждой 500-й непустой строки файла:
Экономия памяти с array.array
Бытует мнение, что array.array работает значительно быстрее, чем обычный list. Это не так: в современных версиях Python список list максимально оптимизирован, и по производительности превосходит array.array в ходе выполнения абсолютного большинста операций — как видно по приведенному ниже примеру, list уступает только в случае с сериализацией. Но вот памяти массивы array.array действительно используют меньше — почти в два раза:
В Python можно разделять функции на внутренние и публичные для оптимизации производительности и удобства использования. Внутренние функции обычно используются только внутри модуля, где они определены, и оптимизированы для быстроты и эффективности. Публичные функции, напротив, предназначены для внешнего использования и могут включать дополнительную проверку данных, логирование и другие механизмы, обеспечивающие удобство и безопасность использования:
Результат:
Декораторы
Декораторы позволяют расширить поведение функции, не изменяя ее исходный код:
Декоратор принимает функцию в качестве входных данных.
Добавляет функциональность, может выполнить какой-то код до и/или после вызова оригинальной функции.
Возвращает модифицированную функцию, которая включает в себя оригинальную функцию плюс дополнительную функциональность.
Декораторы часто используют для:
Логирования — добавляют записи о вызовах функции, ее аргументах и результатах.
Кэширования — сохраняют промежуточные результаты работы функции, чтобы избежать повторных вычислений.
Проверки прав пользователя перед выполнением функции.
Измерения времени выполнения функции.
Этот пример имитирует запросы к внешнему API и показывает, как можно использовать декораторы для кэширования данных и измерения времени выполнения:
Многие начинающие питонисты пренебрежительно относятся к использованию готовых библиотек, — считают это читерством. И совершенно зря: высокопроизводительные библиотеки (например, NumPy и pandas) написаны на C и оптимизированы для максимальной производительности на низком уровне: они эффективно используют память, ресурсы CPU, применяют векторизацию и т.д. В последнее время суперскоростные библиотеки для Python (polars, к примеру) пишут на Rust, у которого еще больше преимуществ, чем у C. Появляются альтернативы для NumPy и pandas, которые могут использовать возможности GPU (например, CuPy) . Всегда, когда это возможно, стоит пользоваться готовыми библиотеками, особенно для ресурсоемких вычислений — это гораздо эффективнее, чем стандартный Python:
Результат:
Короткое замыкание
Короткое замыкание позволяет избежать ненужных вычислений при проверке условий. Этот подход особенно полезен в сложных проверках или при работе с ресурсоемкими операциями: при проверке условия программа прекращает выполнение сразу после того, как найдет первую переменную, удовлетворяющую условию, поэтому следует располагать условия в порядке вероятности срабатывания. Например, для проверки високосного года условия стоит расположить так:
year % 4 == 0 — это условие проверяется первым, так как оно отсеивает большинство не високосных лет. Если год не делится на 4, функция сразу возвращает False.
year % 100 != 0 — если год прошел первую проверку, мы проверяем, не делится ли он на 100. Это условие встречается реже, чем первое. Если год не делится на 100, он точно високосный, и мы можем вернуть True.
year % 400 == 0 — это самое редкое условие, большинство лет до него не доберутся.
Освобождение памяти
Python автоматически управляет сборкой мусора — когда объекты выходят из зоны видимости (например, функция заканчивает работу), все ненужные переменные удаляются, высвобождая оперативную память. Но в приложениях, которые обрабатывают очень большие объемы данных, стоит удалять ненужные объекты сразу, принудительно вызывая сборщик мусора:
Результат:
В этом примере del используется в сочетании с gc.collect() потому, что del уменьшает счетчик ссылок на объект: когда счетчик достигает нуля, объект становится доступным для сборщика мусора.
Короткие сообщения об ошибках
Для встроенных систем (умных устройств и всевозможных IoT-датчиков) стоит использовать короткие сообщения об ошибках — они экономят память и сетевой трафик, а также ускоряют логирование:
Векторизация вместо циклов и списковых включений
Векторизация позволяет выполнять операции над целыми массивами данных одновременно, вместо того, чтобы обрабатывать каждый элемент по отдельности в цикле (или в списковом включении). В зависимости от платформы и типа задачи, векторизация ускоряет обработку данных в десятки и сотни раз:
Результат:
Подведем итоги
Python — интерпретируемый язык, и по этой причине он всегда будет уступать в скорости низкоуровневым компилируемым языкам типа Rust. Однако считать его безнадежно медленным тоже не стоит:
Как показывают приведенные выше примеры, использование памяти и сложные вычисления в Python-приложениях можно значительно оптимизировать.
Есть возможность совместного использования Python и Rust (PyO3).
Стоит также заметить, что иногда лучшим способом оптимизации может стать переосмысление самой задачи и использование принципиально другого подхода к реализации критически важного участка кода, а в этом может помочь хорошее знание готовых алгоритмов и структур данных.
***
Вы уже видели, как важны оптимизация и производительность в Python. Но прежде чем погрузиться в сложные техники, важно освоить базовые навыки программирования. Курс «Основы программирования на Python» от Proglib Academy предлагает:
Комментарии