Ускорение выполнения Python-кода: оптимизация, профилирование и векторизация

0
47

Содержание

Памятка по ускорению Python-кода

  1. Используйте встроенные функции (sum, map, filter) вместо циклов.
  2. Применяйте списковые включения (list comprehensions) для простых преобразований.
  3. Замените циклы на векторизованные операции NumPy.
  4. Профилируйте код перед оптимизацией (cProfile, timeit).
  5. Выбирайте правильные структуры данных (set для поиска, deque для очереди).
  6. Кешируйте результаты функций с помощью lru_cache.
  7. Используйте генераторы для работы с большими потоками данных.
  8. Для CPU-интенсивных задач применяйте Numba, Cython или PyPy.
  9. Для параллельных I/O-задач используйте asyncio или threading.
  10. Избегайте глубокой вложенности циклов и лишнего копирования данных.
  11. Освобождайте память вручную (del) для больших объектов.
  12. Компилируйте код с Nuitka для финальной сборки.

Упаковка переменных

Лучшие способы - изображение номер один
Лучшие способы — изображение номер один

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

В Python для упаковки данных в компактный бинарный формат используется модуль struct:

Внутренние функции

Как ускорить код на - изображение номер два
Как ускорить код на — изображение номер два

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

Декораторы

Python - изображение номер три
Python — изображение номер три

Декораторы позволяют расширить поведение функции, не изменяя ее исходный код:

  • Декоратор принимает функцию в качестве входных данных.
  • Добавляет функциональность, может выполнить какой-то код до и/или после вызова оригинальной функции.
  • Возвращает модифицированную функцию, которая включает в себя оригинальную функцию плюс дополнительную функциональность.
  • Логирования — добавляют записи о вызовах функции, ее аргументах и результатах.
  • Кэширования — сохраняют промежуточные результаты работы функции, чтобы избежать повторных вычислений.
  • Проверки прав пользователя перед выполнением функции.
  • Измерения времени выполнения функции.

Этот пример имитирует запросы к внешнему API и показывает, как можно использовать декораторы для кэширования данных и измерения времени выполнения:

Короткое замыкание

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

  • year % 4 == 0 — это условие проверяется первым, так как оно отсеивает большинство не високосных лет. Если год не делится на 4, функция сразу возвращает False.
  • year % 100!= 0 — если год прошел первую проверку, мы проверяем, не делится ли он на 100. Это условие встречается реже, чем первое. Если год не делится на 100, он точно високосный, и мы можем вернуть True.
  • year % 400 == 0 — это самое редкое условие, большинство лет до него не доберутся.

Короткие сообщения об ошибках

Ускоряем код на - изображение номер пять
Ускоряем код на — изображение номер пять

Для встроенных систем (умных устройств и всевозможных IoT-датчиков) стоит использовать короткие сообщения об ошибках — они экономят память и сетевой трафик, а также ускоряют логирование:

Освобождение памяти

Python & оптимизация времени и памяти / - изображение номер шесть
Python & оптимизация времени и памяти / — изображение номер шесть

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

В этом примере del используется в сочетании с () потому, что del уменьшает счетчик ссылок на объект: когда счетчик достигает нуля, объект становится доступным для сборщика мусора.

Экономия памяти с

Экономия python - изображение номер семь
Экономия python — изображение номер семь

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

Ускорение обработки файлов

Ускорение кода на python в 30 тысяч раз - изображение номер восемь
Ускорение кода на python в 30 тысяч раз — изображение номер восемь

В приложениях, для которых критически важна производительность, необходимо хранить часто используемые данные в памяти и минимизировать операции чтения/записи на диск, чтобы обеспечить высокую скорость извлечения информации. Модуль mmap позволяет работать с файлами на диске, как если бы они находились в оперативной памяти:

Векторизация вместо циклов и списковых включений

#1058 post - изображение номер девять
#1058 post — изображение номер девять

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

List comprehension

Efficient - изображение номер десять
Efficient — изображение номер десять

List comprehension in - изображение номер одиннадцать
List comprehension in — изображение номер одиннадцать

List comprehension — эффективный способ создавать и обрабатывать списки в одной строке. В Python он работает быстрее, чем традиционный цикл for, потому что весь процесс в нём оптимизирован и протекает внутри интерпретатора.

Важно отметить, что, хотя list comprehension обычно быстрее, его не всегда следует использовать. Если вы пытаетесь сделать что-то сложнее обычного преобразования данных, он может быть тяжеловат для понимания. Ясный и читаемый цикл for может быть лучшим выбором.

Map() и lambda-функции

ПОдготовка к - изображение номер двенадцать
ПОдготовка к — изображение номер двенадцать

How to - изображение номер тринадцать
How to — изображение номер тринадцать

map() — встроенная функция, которая всегда используется в комбинации с итерируемой последовательностью, в данном случае со списком. Она идёт по каждому элементу последовательности (то есть к каждому объекту из списка) и применяет к нему функцию, которую приняла первым позиционным аргументом.

map() реализована на более быстром Си. И, как и list comprehension, оптимизирована на уровне интерпретатора Python. То есть при её использовании вы также передаёте всю работу интерпретатору, который оптимизирует выполнение операции.

Кроме того, map() в Python возвращает итератор, а не список. А итераторы — более эффективный способ обработки больших объёмов данных. Они не требуют загрузки всего набора данных в память сразу, а генерируют каждый элемент по мере необходимости. Это значительно снижает потребление памяти, особенно при работе с большими наборами данных.

Lambda-функции в Python — это анонимные функции, которые можно определить прямо в месте использования. То есть вместо того чтобы задавать отдельную именованную функцию через def, можно написать короткую лямбду — прямо в вызове map().

NumPy и Pandas

Correct way to implement piecewise function in pandas / numpy - изображение номер четырнадцать
Correct way to implement piecewise function in pandas / numpy — изображение номер четырнадцать

Python coding tutorial 3 - изображение номер пятнадцать
Python coding tutorial 3 — изображение номер пятнадцать

NumPy — это мощная библиотека, которая предоставляет объекты массивов N-мерного массива с эффективными операциями. При работе с большими массивами данных NumPy может быть намного эффективнее стандартных списков Python благодаря оптимизированным и векторизованным операциям.

Pandas — библиотека, предоставляющая эффективные структуры данных и инструменты анализа данных.

В основном NumPy и Pandas используют для работы с таблицами, когда нужно, допустим, выгрузить Excel-таблицу или рассортировать ячейки. Другие сферы — компьютерное зрение, векторная геометрия и так далее.

Код, в котором используют NumPy и Pandas, часто работает быстрее, чем аналогичный, но написанный с использованием стандартных функций и структур данных Python. И вот по каким причинам:

  1. Большая часть библиотек NumPy и Pandas написана на Cи и C++. Благодаря этому скорость выполнения выше.
  2. Векторизованные операции выполняются сразу над массивами данных, а не поэлементно. Это обеспечивает значительное увеличение производительности, особенно при работе с большими объёмами данных.
  3. Эффективное использование памяти. NumPy и Pandas используют эффективные структуры данных, которые занимают меньше памяти и позволяют выполнять операции быстрее.
  4. Функции оптимизированы для быстрого выполнения на больших объёмах данных.

Профилирование и бенчмаркинг

Большое руководство по ускорению и оптимизации - изображение номер шестнадцать
Большое руководство по ускорению и оптимизации — изображение номер шестнадцать

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

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

По умолчанию timeit выполняет код многократно (1_000_000 раз в примере выше) и возвращает общее время. Можно настраивать число повторов number и даже запускать несколько серий через. timeit удобен, когда нужно сравнить две небольшие функции или выражения – он исключает влияние запуска интерпретатора и делает несколько прогонов для усреднения результата 2 3. Например, если у вас есть два способа вычислить нечто, timeit поможет выбрать более быстрый 4. Недостаток – он не показывает, какая часть кода медленная, а только общее время фрагмента.

• Профилировщик cProfile: встроенный детерминированный профилировщик, который собирает статистику времени выполнения по функциям. С его помощью можно узнать, сколько раз вызывалась каждая функция и сколько времени суммарно она выполнялась 4 5. cProfile можно запустить из командной строки:

python -m cProfile -o my_script.pyЭто сохранит результаты в файл. Затем их можно проанализировать с помощью модуля pstats или визуализировать утилитами вроде SnakeViz 6. Встроенный способ – вызвать () внутри скрипта или обернуть код в () как контекстный менеджер. Например:

Отчет cProfile покажет, какие функции занимают больше всего времени. Однако он выводит много данных, включая вложенные вызовы встроенных функций, что может затруднить анализ в больших приложениях 6. Для упрощения анализа часто используют визуализаторы (тот же SnakeViz) или сужают область профилирования до интересующих частей.

• Line Profiler (line_profiler): внешний инструмент для построчного профилирования функций. Он показывает, сколько времени затрачено на выполнение каждой строки кода в отмеченных функциях 7. Это очень помогает, если нужно понять, внутри функции какая строчка самая медленная. Использование: устанавливаем пакет (pip install line_profiler), отмечаем интересующие функции декоратором @profile, затем запускаем скрипт через утилиту kernprof:

Например, если вы подозреваете, что вложенный цикл тормозит, line_profiler точно покажет, на какой строке цикла программа проводит больше всего времени.

Line_profiler должен быть первым выбором при оптимизации конкретных участков кода 8 9, так как сразу указывает узкие места построчно.

Benchmarking - изображение номер семнадцать
Benchmarking — изображение номер семнадцать

Другие инструменты: существуют и другие профилировщики. Например, memory_profiler для анализа потребления памяти, а также семплирующие

профилировщики вроде PyInstrument (вместо замера каждого вызова, он периодически опрашивает стек вызовов, снижая оверхед) 10. Семплирующие профилировщики меньше нагружают систему и дают сводку по тому, где программа проводит время в целом. Они полезны для долгих приложений (веб-сервисы и пр.), где детальный вывод cProfile избыточен.

Рекомендации по профилированию: всегда начинайте оптимизацию с измерений. Интуиция часто подводит – можно тратить время на ускорение той части кода, которая и так выполняется быстро, игнорируя скрытое узкое место. Поэтому сначала профилируем и определяем, какие функции или участки кода доминируют во времени выполнения. Затем сфокусированно оптимизируем их.

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

Также желательно писать небольшие бенчмарки для критичных функций. Библиотека timeit или IPython-магия %timeit удобны для этой цели. Например, сравнение двух реализаций алгоритма сортировки кастомных объектов с помощью %timeit сразу покажет, какая быстрее, при прочих равных.

Структуры данных и алгоритмы

Комбинаторные алгоритмы часть 3(python для computer science) - изображение номер восемнадцать
Комбинаторные алгоритмы часть 3(python для computer science) — изображение номер восемнадцать

Правильный выбор структур данных и алгоритмов способен дать значительный выигрыш в скорости – куда больший, чем низкоуровневые оптимизации. В Python разные структуры имеют разные сложности операций (см. таблицу сложностей CPython 11 12). Рассмотрим основные структуры и когда их применять:

• Список (list): динамический массив. Хорошо подходит для последовательного хранения и итерации. Получение или изменение элемента по индексу – O(1), добавление в конец – амортизированно O(1), но вставка или удаление в середине/начале – O(n), так как элементы смещаются 13 12. Если нужно часто удалять из начала, список неэффективен – стоит взять (двунаправленная очередь). Итерация по списку – линейная O(n), что обычно быстро на C-уровне, но если нужны частые проверки на принадлежность, у списка они O(n).

• Множество (set) и словарь (dict): хеш-таблицы. Операции поиска, добавления и удаления элемента выполняются за среднее O(1) (константное время) благодаря хешированию. Поэтому, если нужна быстрая проверка принадлежности или поиск по ключу – используйте set / dict вместо списка 14. Например, поиск элемента в списке из 100000 элементов в среднем потребует 50000 проверок, а в множестве – одну хеш-функцию и проверку в таблице. Важно: set не сохраняет порядок (до Python 3.7 dict сохраняет порядок добавления, set – нет). Словари удобны для хранения соответствий “ключ-значение” с быстрым доступом по ключу. Пример: чтобы выяснить, присутствует ли определённое значение в коллекции, лучше делать x in my_set вместо x in my_list – в первом случае операция в среднем константная, во втором – линейная 15. Ниже приведено сравнение времени работы поиска в списке и множестве на миллион элементов:

(В примере элемент присутствует в коллекциях. Проверка в списке ~0.0145 с, во множестве ~0.000001 с – различие на порядки, которое с ростом N ещё увеличивается).

Note: Сложность O(1) для dict/set относится к среднему случаю. В худшем случае (множество коллизий) время может деградировать до O(n), но такого почти не бывает при хорошем хешировании. Также, поиск по значению в словаре (например, val in my_dict.values()) остаётся O(n) – словари оптимизированы для поиска по ключу, а не по значению 16 17.

Двунаправленная очередь (): оптимизирована для операций на обоих концах. Добавление и удаление в начало или конец deque – O(1) 18, тогда как у списка удаление из начала – O(n) (все элементы сдвигаются) 13 19.

Поэтому, если реализуете очередь или обработку потока данных, где требуется делать pop(0) или appendleft, используйте deque. Пример: чтение файлов построчно (где нужно оперативно выкидывать старые элементы) удобно делать через deque, ограничивая её длину, – это будет эффективнее, чем вручную поддерживать список фиксированной длины.

Приоритетная очередь (heapq): реализует минимальную кучу на базе списка. Полезна, когда нужно быстро получать минимальный (или максимальный, с трюком отрицательных значений) элемент из изменяющейся коллекции. Функции

и работают за O(log n) – значительно быстрее, чем, например, сортировка списка при каждом добавлении 20. Используйте heapq, если решаете задачу, где регулярно добавляются элементы и нужно каждый раз брать самый маленький/большой (например, алгоритм Дейкстры, отслеживание Top-K элементов и т.п.). Пример: вместо того чтобы держать отсортированный список из 10000 элементов и вставлять в него через bisect (что O(n)), лучше вести кучу размером 10000 – вставка/ удаление будут O(log n) ~ 14 операций вместо 10000.

Модуль bisect: предоставляет бинарный поиск по отсортированному списку и вставку в него. Операция bisect.bisect_left(a, x) возвращает позицию для вставки элемента x в список a за O(log n), однако сама вставка в список – O(n) (из-за сдвига элементов) 21.

Пояснение структуры кода, который генерирует блочный - изображение номер девятнадцать
Пояснение структуры кода, который генерирует блочный — изображение номер девятнадцать

То есть bisect помогает с поиском индекса (логарифмическое время), но вставлять всё равно приходится средствами списка, что линейно. Вывод: bisect удобен, когда нужно много искать, но мало вставлять. Например, если у вас есть отсортированный список и вы часто делаете поиск диапазона или позицию для элемента – bisect идеален. Но если планируется частое добавление, может быть лучше использовать другую структуру (дерево, сбалансированное бинарное, напр.sortedcontainers из PyPI, или связку bisect+deque). Python-документация прямо отмечает: «для поиска конкретных значений словари эффективнее» 14 (если порядок не важен). Например, вместо бинарного поиска индекса значения в списке, часто можно использовать множество/словарь для теста наличия значения.

Как ускорить приложения на - изображение номер двадцать
Как ускорить приложения на — изображение номер двадцать

– быстрый подсчет элементов (реализован на базе словаря). Для частотного анализа данных предпочитайте Counter(x) или метод.count() у списка (если один элемент) вместо ручного цикла.

(в Python 3.7+ обычный dict уже упорядочен) – если нужен словарь, сохраняющий порядок вставки.

– словарь с значениями по умолчанию, полезен для агрегаций (избегает проверок на наличие ключа, немного ускоряя и упрощая код).

Выбор алгоритмов: кроме структур, важно выбрать эффективный алгоритм.

Урок 3 - изображение номер двадцать один
Урок 3 — изображение номер двадцать один

Например, если задача требует сортировки, использовать встроенную сортировку Timsort (O(n log n)) вместо квадратичных алгоритмов. Если нужно объединить отсортированные списки – эффективнее воспользоваться или (в зависимости от задачи), чем сначала конкатенировать и сортировать заново. Всегда думайте о сложности алгоритма: оптимизация кода не спасёт алгоритм со сложностью O(n^2) на больших входах. Классический пример: наивный алгоритм перемножения матриц O(n^3) против алгоритма Страссена (~O(n^2.8)) – выбор алгоритма даст огромный выигрыш, тогда как микроптимизации внутри O(n^3) мало что изменят. В повседневных задачах: используйте поиск по хеш-таблице вместо вложенных циклов (set вместо списка при множественных проверках включения 22), сортировку за n log n вместо квадратичного обхода, и т.д.

Пример влияния выбора структуры: допустим, вы делаете множество проверок if x in collection. Если collection – список из 100000 элементов, каждая такая проверка в среднем ~50 тыс сравнений. Если заменить на set, проверка – одна операция хеширования и один сравнений. В реальном тесте: поиск элемента в списке занял ~0.15 секунды, в множестве ~0.11 (включая время создания структур) 23 24, но при многократных проверках преимущество set станет подавляющим (как мы показали выше, при 1000 проверках – set быстрее списка в сотни раз). Другой пример: нужно часто удалять первый элемент – список будет каждый раз сдвигать все оставшиеся элементы, это медленно. Дек (deque) решает проблему, выполняя popleft мгновенно 13 19. Эти решения на уровне выбора структуры зачастую дают наибольший прирост производительности.

Эффективное использование стандартной библиотеки и встроенных функций

12 - изображение номер двадцать два
12 — изображение номер двадцать два

Стандартная библиотека Python и встроенные функции написаны на C и оптимизированы, поэтому их использование может заметно ускорить код. Правило: «если есть готовая функция в стандартной библиотеке – используйте её, вместо реализации вручную». Рассмотрим несколько примеров:

• Суммирование и агрегаты: встроенная функция sum(iterable) написана на C и работает быстрее эквивалентного цикла на Python. Пример: сумма миллион элементов циклами vs sum(). Код:

Результат показывает ~7-кратное ускорение при использовании sum вместо ручного цикла (0.20 с против 0.03 с). Аналогично работают min(seq), max(seq), all(seq), any(seq) – они реализованы на C и быстрее, чем эквивалентные Python-циклы. Как отмечается, «встроенные функции Python высоко оптимизированы, что делает их значительно быстрее ручных реализаций на Python»

10 практик кода, ускоряющих выполнение программ на - изображение номер двадцать три
10 практик кода, ускоряющих выполнение программ на — изображение номер двадцать три

Тоесть,старайтесь пользоватьсяготовымиагрегатами:еслинужнопроверить, есть ли хоть один элемент удовлетворяющий условию – используйте any(…) вместо цикла с break; нужно проверить, что все – all(…) вместо цикла и флага; найти максимум – max(); и т.д.

• Генераторы списков и встроенные функции высшего порядка: Встроенные функции высшего порядка (map, filter, zip и т.д.) и генераторные выражения часто более эффективны, чем эквивалентный явный цикл на Python. Причина – они выполнены на уровне C-интерпретатора, уменьшая количество интерпретаций байткода. Например,

map(, data) вызовет для каждого элемента, но сама итерация управляется C-кодом внутри map. List comprehension [func(x) for x in data] близка по скорости, так как тоже подкапотный цикл в С. Оба подхода, как правило, быстрее, чем:

Плюс код лаконичнее. Более того, генераторы позволяют избежать лишних списков в памяти, давая выигрыш по памяти (см. главу про генераторы).

• Конкатенация строк: Классический пример – сборка строки из множества частей. В Python нельзя эффективно накапливать строку через += в цикле, это приводит к квадратичному времени (так как строки неизменяемы, каждое += создает новую строку). Вместо этого используйте “”.join(list_of_strings).

берет срез итератора;, combinations, groupby и др. – позволяют решать задачи перебора и группировки очень эффективно. Вместо вложенных циклов для декартова произведения списков, используйте – он написан на C и более производителен. Пример: Нужно

вычислить комбинации пар элементов из двух списков: [(a,b) for a in A for b in B] – Python-средство (list comprehension) уже хорошо, ноlist((A,B)) будет не хуже, а если не нужен сразу весь список, даст ленивый итератор, что сэкономит память.

•Модуль math и другие C-библиотеки: Если нужно выполнить математическую операцию, по возможности берите реализацию из модуля math или statistics. Например, (x) (обертка над С sqrt) быстрее, чем x**0.5 (хотя последнее тоже быстро, но избегает создания промежуточного Python-объекта для результата). (n) гораздо быстрее самописного вычисления факториала. Стандартный модуль statistics предоставляет C-оптимизированные вычисления среднего, медианы и т.д. – тоже лучше, чем писать самим.

• Использование zip и распаковки: При одновременном обходе нескольких коллекций используйте zip – он также работает на С и отдаёт элементы кортежами без создания промежуточных списков. Например, for x, y in zip(list1, list2): … быстрее, чем ручной индексный обход.

•Кэширование и повторное использование объектов: Стандартная библиотека предоставляет инструменты кеширования (см. главу о кешировании). Например, вместо вызова сложной функции с одними и теми же параметрами в цикле можно декорировать её functools.lru_cache и получить результат из кеша (если это приемлемо с точки зрения логики).

• Встроенные типы и методы: Используйте методы списков, словарей, строк. Например, чтобы удалить все элементы из списка – () быстрее, чем присваивать новый список. Чтобы перевернуть список – () (in-place) или reversed(lst) (итератор) вместо среза lst[::-1] (который создаёт копию). Для сортировки списка in- place: () эффективнее, чем sorted(lst) если копия не нужна. Для поиска подстроки в строке – метод.find() или оператор in (они реализованы на C, с алгоритмом Boyer-Moore-Horspool), нежели писать свой цикл.

В целом, перечитайте возможности стандартной библиотеки – возможно, ваша задача уже решается встроенной функцией. Как пишет один источник, «используйте возможности Python и библиотек, а не изобретайте велосипед – это и короче, и быстрее» 27. Например, нужно посчитать уникальные элементы – вместо цикла и списков используйте set или Counter. Нужно отсортировать по ключу – воспользуйтесь параметром key в sorted (он тоже в C проходит). Python – язык высокого уровня, и его сила в том числе в богатой оптимизированной библиотеке. Дальше В этом руководстве по ускорению и оптимизации Python-кода мы рассмотрим работу с генераторами.

Оптимизация циклов и генераторов

Представляем specialist - пакет для оптимизаций - изображение номер двадцать четыре
Представляем specialist — пакет для оптимизаций — изображение номер двадцать четыре

Циклы – частый источник замедления в Python, потому что каждая итерация выполняется интерпретатором. Уменьшение количества операций в цикле или перевод цикла на уровень C может существенно ускорить код. Рассмотрим техники оптимизации циклов:

• List Comprehensions (генераторные выражения для списков): В Python принято использовать генераторы списков вместо явных циклов, когда это возможно. Они не только делают код короче, но и работают быстрее, так как цикл выполнен внутри интерпретатора на нижнем уровне. Пример: сравним заполнение списка квадратами чисел обычным циклом и list comprehension:

По замерам, генератор списков опережает цикл с.append() примерно в 1.6 раза на миллион элементов 28 29 (0.17 с против 0.10 с в тесте 28). Причина – append вызывает метод на каждом шаге, тогда как в comprehension всё происходит внутри C-цикла. Поэтому для преобразования или фильтрации последовательностей старайтесь использовать comprehensions или встроенные функции вместо ручных loops. Например, filtered = [x for x in data if condition(x)] быстрее, чем for + if с append.

• Функции map и filter: Они могут быть чуть более эффективны, чем эквивалентные генераторы списков, если передаваемая функция – встроенная или оптимизированная. Например, map(, list_of_strings) может быть быстрее

filter возвращают lazy-итераторы, поэтому если нужен список – не забудьте обернуть в

list(…). В целом, comprehension чаще предпочтительнее из соображений читаемости, но знание map/filter не помешает – иногда они вписываются элегантно.

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

Здесь (25) вычисляется каждую итерацию, хотя результат всегда 5 – вынесите это за цикл. Или, если внутри цикла часто происходит обращение к атрибуту или глобальной переменной, можно закешировать ссылку в локальную переменную. Например:

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

• Использование enumerate и zip вместо ручного управления индексами: Если нужен индекс в цикле, не делайте for i in range(len(seq)): и затем seq[i] – это и длиннее, и потенциально медленнее (Python вынужден каждый раз вычислять len(seq) и делать индексацию). Правильнее: for i, val in enumerate(seq): ….

Функция enumerate написана на C и итерируется по последовательности эффективно, возвращая кортежи (index, value). Аналогично, если нужно итерироваться синхронно по двум и более коллекциям, используйте zip. Это не столько про скорость (хотя она тоже выигрывает), сколько про читаемость и сокращение возможностей ошибки.

• Generator Expressions (ленивые генераторы): Вместо создания промежуточных списков, можно использовать генератор-итератор. Например, sum(x*x for x in data) – здесь мы не создаём список всех квадратов, а генерируем их по одному и сразу суммируем. Это экономит память и, возможно, время на выделение/освобождение памяти, хотя сами вычисления занимают столько же. Генераторы полезны, когда объем данных большой и нет нужды держать все результаты одновременно. В пайплайнах обработки данных можно передавать генератор от одной стадии к другой, избегая создания громоздких временных списков. Помните, что один проход по генератору истощает его – но именно это зачастую и нужно (единственный проход). Например, чтение большого файла: sum(1 for _ in open(‘’)) – посчитает число строк без хранения их в памяти (в отличие от len(open(‘’).readlines())).

•Избегайте ненужных вложенных циклов Python: Если возможно, переводите вложенные итерации на C-уровень. Пример: у вас вложенный цикл для обработки пар элементов – рассмотрите возможность использовать функции из itertools (как product, combinations) или numpy для числовых вычислений (см. раздел про NumPy). Вложенный Python-цикл – двойной удар по производительности (квадратичная сложность + высокий overhead интерпретатора на каждую итерацию). Иногда можно переписать двойной цикл через более эффективные средства. К примеру, поиск пересечений множеств из двух списков: вместо двойного цикла for x in a: for y in b: … стоит преобразовать один список во множество и просто проверить каждого кандидата из другого на принадлежность (получится O(n+m) вместо O(nm)). Это уже переключениеалгоритма/структуры, но демонстрирует подход: по возможности, не делать во вложенном Python-цикле то, что можно сделать более оптимально*.

ЧИТАТЬ ТАКЖЕ:  Передача лямбда-функции в качестве аргумента в Python: примеры с map, filter, sorted и reduce

• Векторизация и NumPy: Отдельно отметим – если ваши циклы связаны с числовыми расчетами над большими масивами данных, очень вероятно, что лучше использовать библиотеку NumPy, которая выполняет циклы на уровне С (см. раздел 9). Векторизированные операции могут ускорить вычисления на порядки, поэтому вместо того, чтобы перебирать массивы Python-циклами, попробуйте сформулировать операцию над целыми массивами/векторами.

Пример: оптимизация цикла через генераторное выражение. Имеем список цен prices и хотим получить список с налогом 20%. В лоб (цикл):

Кроме более лаконичного вида, второй вариант будет быстрее. А если конечный список нам не нужен, а, скажем, нужно просуммировать новые цены: total = sum(p * 1.2 for p in prices). Это избежит создания new_prices в принципе.

Итог: используйте идиоматические конструкции Python для циклов – list/dict/set comprehensions, генераторы, функции any/all/map/filter/zip вместо ручных развёрнутых циклов. Это не только ускоряет выполнение, но и улучшает читаемость. Как заметил один разработчик: «код пишется для людей, а не для машин. Если нужно ускорение – чаще дело в выборе алгоритма, а не в том, как именно написать цикл» 30. То есть, сначала сделайте код простым и понятным, а затем измерьте и оптимизируйте самые медленные циклы известными методами.

Ускорение кода с помощью Numba, Cython и PyPy

Эти фишки реально ускорят - изображение номер двадцать пять
Эти фишки реально ускорят — изображение номер двадцать пять

Иногда, несмотря на все оптимизации, Python-код остаётся слишком медленным из-за ограничений интерпретатора и GIL. Для вычислительно тяжёлых задач стоит рассмотреть альтернативные исполнения или компиляцию критичных участков:

• Numba: JIT-компилятор (just-in-time) для Python, особенно эффективный в области численных вычислений. С помощью Numba можно компилировать функции Python в машинный код во время исполнения, добавив декоратор @ (no-python mode). Numba отлично ускоряет циклы с числовыми расчётами, работу с массивами NumPy и прочий научный код. Пример использования:

Первая вызов функции может занять время (компиляция), но последующие выполняются на скоростях, близких к C. Преимущество Numba – не требует переписывать код на другом языке, вы просто помечаете функцию декоратором. Конечно, есть ограничения: Numba хорошо работает с типичными циклами, арифметикой, NumPy-операциями, но не поддерживает весь спектр Python (например, произвольные встроенные объекты, сложные динамические типы). Для численного кода выигрыши огромны: были случаи, где простой декоратор давал ускорение в 1000 раз относительно чистого Python 31! Например, расчет расстояний между парой тысяч 3D- точек: на Python ~12 секунд, с Numba – 15 миллисекунд 32 33. Это три порядка разницы за счёт JIT-компиляции. На практике, Numba часто обеспечивает 5x-100x ускорение в численных задачах.

Если ваш проект – научные вычисления, обработка сигналов, финансовые расчёты и т.п., где основная нагрузка – циклы и математика, Numba – первый кандидат. Он компилирует функции в машинный код через LLVM. Кстати, Numba можно применять и к функциям, работающим с массивами NumPy – тогда ускорение особенное хорошее, т.к. Numba может убрать сам overhead Python при работе с массивами.

• Cython: Это надмножество Python, позволяющее компилировать код в С-расширение. Вы можете постепенно превратить «горячую» функцию в Cython-версию, добавляя статические типы для переменных, и добиться очень высокой скорости – вплоть до скорости Си (если все тяжелые операции переведены на типы C). Например, тот же суммирование квадратов можно написать в Cython как:

Скомпилировав этот код, получаем C-расширение. Cython требует некоторого порога вхождения: нужно установить компилятор, прописать или использовать %%cython -магию в Jupyter. Зато Cython позволяет постепенно оптимизировать: сначала вы можете просто скомпилировать существующую Python-функцию – выигрыш будет небольшой, может 1.2-2x из-за устранения байткод-оверхеда. Затем добавлять аннотации типов – скорость будет расти по мере устранения динамических операций. Например, в одном сравнении Numba и Cython для вложенных циклов: Cython-версия при тщательной оптимизации оказалась ~30% быстрее Numba.

То есть при должном усилии Cython даёт максимальный контроль и производительность. Также с Cython можно вызывать напрямую C-функции, работать с указателями, чтобы обойтись без Python-объектов внутри критичных частей.

Cython активно используется для ускорения библиотек: многие части Pandas, Scikit-Learn написаны на Cython. Минус – требуется поддерживать сгенерированный бинарный модуль (т.е. компиляция при установке), и код с явными типами уже менее «чистый» Python. Однако для ключевых участков (например, парсер, внутренний цикл алгоритма) это оправдано. Выигрыши: типичный ускорение после аннотации типов 10x-50x и более (зависит от природы задачи). Без аннотации – незначительно. Поэтому, если решите применять Cython, стоит пойти до конца: выписать типы, отключить проверки границ списков/массивов (boundscheck=False), отключить negative-index wrap (wraparound=False) и т.п., чтобы убрать весь питоновский оверхед. Тогда производительность может выйти на уровень Си.

• PyPy: альтернативная реализация Python с JIT-компиляцией. В отличие от CPython, PyPy во время работы оптимизирует часто выполняемые участки кода. Подключение – минимальное: достаточно установить PyPy и запустить ваш скрипт под ним (обычно pypy3). Для чисто алгоритмического кода PyPy может дать ускорение в 4-7 раз в среднем 35 по сравнению с CPython, а иногда и больше. В одном тесте (pystone) PyPy показал в ~3 раза больше операций, чем CPython, и ~в 3 раза больше, чем даже скомпилированный Nuitka-бинарник 36 35. Официально заявляется ускорение PyPy ~7.6x на среднем наборе бенчмарков 35. Преимущество PyPy – не требуются никакие изменения кода. Вы просто используете другой интерпретатор.

Однако есть нюансы: PyPy особенно эффективен на долгоживущих процессах с интенсивными вычислениями на уровне Python (например, многократные вызовы функций, работа с чисто питоновскими структурами). Если ваш код активно использует библиотеки с C-расширениями (NumPy, Pandas, SciPy и т.д.), выигрыш будет меньше, а иногда PyPy даже может быть медленнее при вызове C-расширений (так как они не JIT-оптимизируются и могут иметь несовместимости). Например, для I/O-ограниченных приложений (ожидание сетевых операций, диска) PyPy мало что даст – там не CPU-bound работа. Но для таких задач, как парсинг большого JSON, обработка текста, алгоритмы, не использующие много внешних C библиотек – PyPy может себя проявить отлично.

На 2026 год PyPy поддерживает Python 3.9 (экспериментально 3.10). Если все зависимости вашего проекта совместимы, попробуйте PyPy: «Если ваш код работает под PyPy – просто используйте его и радуйтесь» 37. Пример: есть расчёт с тяжелым использованием классов и рекурсии – PyPy оптимизирует его гораздо лучше CPython, и без усилий. Но, например, вычисление NumPy- матрицы под PyPy не быстрее CPython, потому что узкое место – внутри C, а PyPy не ускоряет C код.

  • Сравнение и выбор инструмента: И Numba, и Cython, и PyPy – решают схожую проблему, но по-разному:
  • Numba – минимальный барьер входа для численных расчётов. Если у вас функция с циклами, матрицами, сложной арифметикой – декоратор @njit может мгновенно дать 10x-100x ускорение. Минусы: не поддерживает сложный Python (например, генераторы, много типов), требует установки (но pure Python fallback возможен).
  • Cython – требует писать (или генерировать) C-подобный код, но работает везде (не только для чисел) и даёт максимальный контроль. Хорош при создании библиотек, модулей, которые потом будут переиспользоваться. Для разовой оптимизации скрипта – возможно избыточен, если можно применить Numba или PyPy.
  • PyPy – прост в применении, ускоряет большую часть Python-кода автоматически. Хорош для долгих сервисов, серверов, где JIT успеет разогнаться. Не требует компиляции кода (JIT происходит в рантайме). Минус – не все библиотеки совместимы, и использовать PyPy в некоторых средах (например, AWS Lambda, GCP Functions) может быть сложно. Также PyPy потребляет больше памяти обычно.

Пример кейса: Вы написали сложный алгоритм динамического программирования на Python, работает медленно. Пробуете PyPy – он в 3 раза ускоряет. Этого недостаточно. Тогда можно переписать критичный внутренний цикл на Cython или применять Numba – получаете еще порядок-два ускорения. Комбинировать тоже можно: например, есть успешные случаи использования Numba внутри PyPy, но это экзотика. Обычно выбирают что-то одно.

• Другие компиляторы: Существуют и другие проекты – Nuitka, Pythran, MyPyC, Transcrypt и т.д. Nuitka компилирует весь Python-скрипт в C++ код и затем в exe (см. раздел 10), давая ~2x ускорение на уровне всего приложения 38, но не обеспечивая кратный рост производительности критичных участков (GIL остаётся). MyPyC компилирует типизированный Python (аннотации typing) в С для ускорения. Pythran – компилятор для векторных вычислений (особенно интеграция с NumPy). Для широкого применения Numba/Cython/PyPy остаются топ-инструментами.

Практический совет: начинайте оптимизацию с самого простого – попробуйте запустить под PyPy. Если не дало нужного эффекта или несовместимо – профилируйте и подумайте о переносе самых тяжёлых функций на Numba или Cython. Например, «Numba ускоряет Python-код почти до C-скорости одной строкой, и лишь немного медленнее тщательно оптимизированного Cython- кода» 31 – это впечатляет и экономит время. Но Numba хорош не для всего – например, он не умеет в JIT многопоточный код или работа с Python-списками (только с типизированными List из). Тогда ваш выбор – Cython или переписать на C.

В итоге, комбинация: использовать CPython + оптимизации кода, затем PyPy или JIT- компиляция отдельных функций, позволяет достичь отличных результатов, сохраняя большую часть кода на Python. Не забывайте, что Python развивается – в версии 3.11, например, появились существенные внутренние оптимизации, ускорившие интерпретатор на ~25-60% на ряде задач

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

Параллелизм и асинхронность: потоки, процессы, asyncio

Асинхронность, многопоточность, многопроцессность в python - изображение номер двадцать шесть
Асинхронность, многопоточность, многопроцессность в python — изображение номер двадцать шесть

Современные вычислительные задачи часто можно ускорить, выполняя их параллельно или конкурентно. В Python есть несколько моделей параллелизма:

• Многопоточность (Threading): Модуль threading позволяет запускать несколько потоков (threads) внутри одного процесса. Однако в CPython есть Глобальная блокировка интерпретатора (GIL), которая не позволяет одновременно выполняться более чем одному потоку Python-байткода в один момент времени 40 41. Это значит, что потоки не ускоряют CPU-bound задачи – они будут выполняться по очереди (хотя могут чередоваться). Но для I/O-bound задач (сетевые запросы, ожидание файловой системы, спящее время) GIL автоматически освобождается на время операций ввода-вывода, и другие потоки могут выполняться. Поэтому потоки пригодны, когда вы ждёте внешних ресурсов и хотите в это время заняться чем-то ещё. Например: загрузка данных из интернета – можно запустить 10 потоков, каждый делает запрос, и пока 9 ждут ответа, 1 может обрабатывать свой ответ. В итоге общее время ~равно времени самого медленного запроса, а не сумме.

Когда использовать потоки: – Для задач, ограниченных ожиданием: сетевые запросы (HTTP, сокеты), работа с БД (если драйвер наружу GIL), ожидание ввода пользователя, Sleep и т.п. – Для параллельного обслуживания нескольких клиентов (каждый в своём потоке) – классический подход в веб-серверах до появления async. – Когда нужно упростить структуру программы (например, отделить поток обработки GUI от фонового расчёта).

Не использовать потоки для: – Тяжёлых вычислений на Python (число Pi в миллиард знаков, обработка огромного списка в цикле) – GIL не даст выигрыш, здесь помогут процессы или переход на C/Numba. – Задач, где нужен истинный параллелизм на нескольких ядрах.

Пример, когда потоки полезны: у вас есть функция, которая парсит 1000 URL-адресов с помощью библиотеки requests (которая блокирующая). Если делать последовательно, суммарное время ~1000 * (среднее время отклика). Если же создать пул потоков и параллельно запрашивать десятки URL, общее время ~1000/10 * (среднее время) – почти в 10 раз быстрее (с учётом накладных). Это типичный I/O-bound сценарий. Важно отметить, что потоки могут взаимодействовать через общую память (общие переменные, структуры) – это плюс по сравнению с процессами, но накладывает ответственность: нужна синхронизация (lock, Queue) для доступа из разных потоков, иначе можно получить race conditions.

Библиотеки: Для удобства можно использовать, чтобы запускать функции в пуле потоков, не работая напрямую с классом Thread. Этот подход позволяет легко распараллелить работу по списку задач (например, (fetch_url, urls) запустит параллельно fetch_url для каждого URL).

• Многопроцессность (Multiprocessing): Модуль multiprocessing запускает несколько процессов (внешне похоже на потоки, но это отдельные процессы с собственной памятью). Здесь каждый процесс имеет свой интерпретатор и GIL, поэтому процессы могут реально выполняться параллельно на разных ядрах. Это подходит для CPU-bound задач, разгружающих одно ядро на несколько. Например, у вас 8-ядерный CPU – 8 процессов Python могут в теории выполнить задачу ~в 8 раз быстрее, чем один (на практике неидеально из-за накладных). multiprocessing предоставляет интерфейс, схожий с threading: Process классы, пулы (), очереди и конвейеры для обмена данными.

Когда использовать процессы: – Вычислительно интенсивные задачи, где данные можно разделить между процессами. Например, обрабатывать элементы массива по частям, тренировать несколько моделей машинного обучения параллельно, рендеринг частей изображения и т.п. – Когда нужно обойти GIL, но остаться на Python (не переписывая на С). Процессы – универсальный способ параллелить всё, что угодно, ценой дополнительной памяти.

Недостатки процессов: – Межпроцессное взаимодействие медленнее, чем в потоках. Общей памяти нет (по умолчанию), приходиться использовать сериализацию (pickle) для передачи данных через или Pipe. Это добавляет оверхед. Тоже самое с

: аргументы и результаты функций сериализуются. Для больших объектов это накладно. – Создание процесса существенно тяжелее потока (инициализируется новый интерпретатор, загружаются модули). Хотя модуль поддерживает fork на Unix, что быстрее, но всё равно overhead больше, чем у потока.

Тем не менее, для CPU-bound ничего другого нет (пока есть GIL). Например, у вас задача – обработать 1000 изображений по сложному алгоритму. На чистом Python в одном потоке это займёт час, а на 4 процессах – около 15 минут, почти линейное ускорение (если диск успевает

снабжать их данными). Пример: рендеринг фрактала – можно разделить область на 8 частей и посчитать каждым процессом свою часть, потом собрать. Или генерация отчётов: запустить 4 процесса, каждый формирует поднабор.

Инструменты: и позволяют писать параллельный код высокоуровнево. Например, with ProcessPoolExecutor() as ex: (f, data, chunksize=10) – выполнит f(x) для элементов data, распределяя между процессами. Важно: функция f и объекты data должны быть

На последовательном варианте Python использует 1 ядро и, скажем, тратит 8 секунд на 4 вызова. На параллельном – все 4 ядра, и завершается за ~2.2 секунды (с учётом накладных) – ускорение ~3.6x близко к числу ядер. Если бы мы попробовали то же с ThreadPoolExecutor (т.е. потоками) – время осталось бы ~8 секунд из-за GIL (фактически потоки выполнились бы по очереди).

• Asyncio (асинхронность): Это третий подход – асинхронное программирование, появившееся в Python 3. Он не использует несколько системных потоков, а выполняет задачи кооперативно в одном потоке, переключаясь между ними. Асинхронность особенно эффективна для большого числа параллельных I/O задач (сетевые запросы, взаимодействие с базой, ожидание таймеров). Программист пишет функции с async def и использует await для операций, которые могут занять время (например, запрос к серверу). Когда такая операция ожидает, управление возвращается в цикл событий, который может запустить другую задачу. Таким образом, один поток может обслуживать тысячи соединений эффективно, не создавая тысячи потоков ОС.

Плюсы asyncio: – Малые накладные на переключение между задачами (не нужен контекст свитч ОС, всё в одном потоке). – Отсутствие GIL-проблем – так как всё и так в одном потоке, GIL не мешает (но и параллельности по CPU нет, это concurrency, а не parallelism). – Отлично масштабируется по числу ожиданий: например, можно держать 10k открытых сокетов и ждать их – потоки бы на таком количестве исчерпали ресурсы системы, а в asyncio это нормально.

Минусы: – Сложнее писать и отлаживать, непривычный синтаксис (хотя уже стал стандартным). – Код, выполняющийся внутри async def не должен блокировать поток надолго (иначе он застопорит весь цикл). То есть CPU-bound вычисления не стоит делать внутри asyncio задач – для них надо выносить в ThreadPool/ProcessPool (asyncio предоставляет loop.run_in_executor). – Библиотеки должны поддерживать async (но сейчас их много: aiohttp для HTTP, asyncpg для PostgreSQL, и т.д.).

Когда использовать asyncio: – В высокопроизводительных сетевых серверах (протоколы, веб- серверы). Многие современные веб-фреймворки (FastAPI, aiohttp) построены на async и могут выдерживать большой tps с малой задержкой. – В клиентах, делающих много одновременных запросов. Например, скачивание с десятков API одновременно. – Когда нужно эффективно ждать сразу множество событий (таймауты, сокеты, оконные события).

Пример (aiohttp): Одно из сравнений HTTP-клиентов показало, что «AIOHTTP примерно в 10 раз быстрее requests при высоком числе одновременных запросов» 42. Requests – синхронная библиотека: пока один запрос не выполнится, другой не начнётся (если не использовать потоки). AIOHTTP – асинхронная: позволяет выдать сотни запросов параллельно и обрабатывать ответы по мере прихода. В итоге достигается Throughput, невозможный для single-thread sync подхода. В реальном тесте, AIOHTTP показал ~240 запросов/с против ~20 запросов/с у requests (при тесте 1000запросов)–тосамое10-кратноепревосходство 42.И~1.5-кратноепревосходствонадHTTPX (другой современный клиент) благодаря более низким накладным.

Сетевые фреймворки: Для асинхронности часто используют готовые решения: например, для веб-сервера – asyncio.start_server, высокоуровневые библиотеки типа aiohttp (веб- сервер и клиент), quart (async аналог Flask), Sanic, для высокоуровневого HTTP-клиента – aiohttp или httpx.

Threading vs Asyncio: Они оба подходят для I/O-bound задач, но: – Если у вас относительно немного параллельных операций (десятки), и код уже написан синхронно – проще использовать threads (можно прямо через ThreadPoolExecutor). – Если параллельных операций очень много (сотни, тысячи) – threads начнут «тяжелеть» (память, расписание ОС). Asyncio лучше масштабируется и предоставляет больше контроля. – Asyncio может дать лучше латентность при большом числе задач, потому что события обрабатываются своевременно, а не ждет пока ОС проснёт спящий поток. – Разработка с asyncio требует асинхронных версий библиотек (DB драйверов, HTTP клиентов). Если их нет, придётся использовать потоки под капотом (что тоже допустимо).

Отличный пример: представьте чат-сервер на 10k клиентов. Реализуя его на потоках, вам нужно 10000 потоков – это почти нереально, потоки будут больше переключаться, чем работать. На asyncio – один поток, 10000 Task’ов, которые большую часть времени ждут (пока пользователи что-то напишут). Результат: минимальные затраты CPU и памяти.

• GIL и настоящий параллелизм: Из-за GIL, только процессы дают настоящий параллелизм на нескольких ядрах. Однако, стоит упомянуть, что некоторые вычисления, хоть и CPU-bound, могут параллелиться и в потоках, если они выпускают GIL. Например, многие операции NumPy освобождают GIL (так как выполняются в C). Поэтому можно, например, параллельно в потоках выполнять матричные умножения NumPy – это сработает (каждый поток внутри NumPy займёт своё ядро). Но такой подход редко нужен, так как NumPy сам может использовать многопоточность внутри (через OpenBLAS, MKL).

Выбор модели: – Для CPU-bound задач: multiprocessing (или сторонние, вроде Joblib, Ray, Dask) – либо распараллелить на уровне C/NumPy. – Для I/O-bound с небольшим числом параллельных блокировок (до сотен): проще использовать многопоточность. – Для I/O-bound с очень большим числом открытых соединений/задач: лучше asyncio. – Комбинации: например, можно использовать asyncio для сетевого сервера + ProcessPoolExecutor для отправки тяжелых CPU задач в отдельные процессы, не блокируя цикл.

Пример практики: Веб-приложение, обрабатывающее запросы: можно запустить несколько процессов (workers) для использования всех ядер, а внутри каждого использовать asyncio или потоки для взаимодействия с БД и внешними сервисами. В итоге – масштабирование и по CPU, и по числу параллельных I/O.

Важно: Параллелизм усложняет разработку. Нужно учитывать потокобезопасность (в потоках), т.е. защищать общие данные локами или использовать thread-safe структуры (очереди). В процессах – научиться быстро сериализовать данные (может, использовать общую память

/Array или, если нужно часто обмениваться). Asyncio требует особого внимания к тому, где ставить await (чтобы не блокировать цикл).

Но результат может сильно повысить производительность приложения. Как отмечено: «многопоточность подходит для I/O-bound активностей» 40, «multiprocessing – для CPU- bound» 43. Правильное сочетание дает масштабируемость по нагрузке.

Кеширование и мемоизация

How - изображение номер двадцать семь
How — изображение номер двадцать семь

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

• functools.lru_cache: Декоратор из стандартной библиотеки, реализующий LRU-кеш (кеш с вытеснением least recently used при переполнении). Использование крайне простое:

Здесь fib при каждом новом значении n вычислит результат (рекурсивно, с принтом), но повторно для тех же n сразу отдаст из кеша. LRU-кеш имеет фиксированный maxsize (последние 128 результатов, в примере), но можно указать None для безразмерного кеша (осторожно с памятью!). lru_cache делает функцию непрозрачно – т.е. для пользователя это все та же функция fib(n), но внутри результаты хранятся в словаре. Декоратор потокобезопасен (использует лок для кеша), поэтому работает и в многопоточной среде.

Эффект: кеширование способно ускорить повторные вызовы функции в тысячи раз, если она изначально дорогая. Например, вычисление 35-го числа Фибоначчи наивно рекурсивно (~30 млн операций) занимает секунды, а с кешированием – почти мгновенно, так как рекурсия свернётся, повторно не вычисляя подзадачи. В общем случае, выигрыш зависит от того, как часто повторяются входные данные.

Пример реальный: в веб-приложении есть страница отчетов, формирующаяся секундами. Если данные меняются раз в час, нет смысла генерировать её каждый раз – можно кешировать результат генерации на 10 минут. Тогда первые пользователи подождут, но следующие получат ответ почти мгновенно (из памяти или быстрой БД). Cameron MacLeod приводит пример Flask- приложения, где без кеша страница генерировалась ~171 мс, а с применением @lru_cache – ~13.7 мс 46 47, т.е. улучшение более чем в 12 раз просто за счёт предотвращения повторного вычисления шаблона для тех же данных.

• Ручное мемоизация словарём: lru_cache – удобный способ для чистых функций. Если условия не позволяют (например, вы хотите контролировать жизненный цикл кеша или функция непредсказуемое количество аргументов), можно реализовать кеш самостоятельно: просто хранить результаты в глобальном словаре. Например:

Это даёт полный контроль – можно очищать кеш по условиям, ограничивать размер и т.д. Однако это всё уже делает lru_cache, потому ручной подход нужен редко, в специфичных ситуациях.

• Внешние системы кеширования: В веб-разработке часто применяются внешние кеши – например, Redis или Memcached. Они позволяют сохранять кэшированные данные во внешней памяти (обычно RAM-сервер), чтобы делиться между разными процессами и даже серверами. Например, результаты тяжёлых запросов к БД можно складывать в Redis: при повторном запросе сервис проверяет кеш – если есть, возвращает мгновенно; если нет, идёт в БД. Это снижает нагрузку на базу и ускоряет отклик. Redis – очень быстрый (миллионы ops/sec) – фактически предоставляет константное время доступа к кэшу по ключу через сеть. По опыту, Redis позволяет снизить время ответов на порядки в ситуациях «одно и то же спрашивают много раз». Как сказано, «Redis позволяет снизить нагрузку на основную базу данных, одновременно ускоряя чтение данных» 48. Ключ – не забывать про актуальность: кеш может устаревать, поэтому практикуют TTL (time-to-live) – время жизни записи, или инвалидируют кеш при изменениях (например, удалять ключ из кеша, когда в БД обновлены данные).

Пример: страница каталога товаров делает сложные агрегации. Без кеша – 2 секунды, с кешем (в Redis, ключ зависит от параметров фильтра) – 50 миллисекунд. Это реальный выигрыш, который чувствуют пользователи. Или кэширование токенов авторизации, чтобы не расшифровывать их каждый раз; кеширование геокодирования адресов (Google API дорогой: лучше один раз перевести адрес в координаты и хранить).

• LRU vs LFU vs TTL: Алгоритмы кеша бывают разными (Least Recently Used, Least Frequently Used и т.д.). functools.lru_cache – LRU. В Redis по умолчанию можно настроить разные политики. Важно подобрать maxsize или срок жизни так, чтобы кеш не разрастался бесконечно, но и удерживал часто используемые данные. LRU хорошо работает, когда недавние вычисления вероятно пригодятся снова (часто так и есть). LFU – когда интереснее частота обращений.

• Кеширование и память: Кеш – это обмен памяти на время. Храним результаты, чтобы не тратить время на перерасчёт. Нужно следить, чтобы кеш не съел всю память (особенно безразмерный). lru_cache с maxsize решает, Redis можно настроить на определенный объем (eviction policy). Если данных очень много и они объемны, можно кешировать на диск (но тогда теряется часть выгоды, хотя все равно может быть быстрее чем пересчет).

Мемоизация для рекурсивных алгоритмов: Классическое применение – фибоначчи, как выше, или сложные рекуррентные формулы (например, динамическое программирование, где вместо таблички можно мемоизировать рекурсивную функцию). Это превращает экспоненциальное время в линейное. С лру_cache реализовать DP очень просто: вы пишете рекурсию по определению, и декорируете – всё, работает как DP с мемоизацией.

Кеширование I/O: Кешируют не только вычисления, но и результаты чтения. Например, если из файла или HTTP часто читаются одни и те же данные, можно держать их в памяти (в кеше) вместо повторного чтения. Многие ОС кешируют дисковые данные на уровне системного Page Cache, но на прикладном уровне тоже можно – например, парсинг конфиг-файла: читать его каждый раз – медленно, лучше один раз загрузить и хранить.

functools.lru_cache – лучший старт для мемоизации функций (краткоживущих объектов).

cachetools (внешняя библиотека) – предлагает разные политики кеша (LFUCache, TTLCache), удобно если нужны TTL или ограничение по времени.

– для ML задач, кеширует результаты функций на диск (используется в scikit-learn для кеширования промежуточных шагов).

diskcache – еще одна библиотека, которая легко кеширует данные на диск с алгоритмом, близким к LRU, хороша когда объем кеша превышает RAM.

Показывает, что второй вызов был hit из кеша. Ускорение – практически бесконечное (0 времени против 2 сек).

Кеширование в многопоточных/многопроцессных приложениях: lru_cache сохранит кеш на процесс. В многопоточных это ок (разделяется между потоками, с локом). В multiprocess – каждый процесс будет иметь свой отдельный кеш, что не всегда желаемо. Тогда имеет смысл вынести кеш в общий внешний (Redis) или запускать процессы-воркеры без локального кеша, но перед ними держать прокси, читающую из единого кеша.

Не забывайте про обновление кеша: кеширование вводит такую проблему, как устаревание данных. Нужно продумать, как валидировать кеш: либо по времени (TTL), либо по событиям (например, очистить ключ при изменении источника). Иначе пользователи могут получать неактуальные данные. В задачах, где допустима некоторая устарелость (например, кешировать результаты за последний час) – TTL в помощь. В задачах, требующих свежести, можно комбинировать: отдавать из кеша, но в фоне обновлять.

Заключение: кеширование – простой в реализации способ ускорить повторяющиеся операции. Как говорится: «кеширование может дать огромный буст производительности, цена – потребление памяти и сложность обеспечения актуальности данных». Во многих системах (веб, распределенные сервисы) без кешей не достичь нужного throughput. Используйте lru_cache для быстрого локального результата или подключайте внешние кеш-системы для больших задач – они существенно сокращают нагрузку и время отклика 49.

ЧИТАТЬ ТАКЖЕ:  Извлечение даты из Datetime в Python без времени: pandas.to_datetime и другие методы

Работа с большими объемами данных: Pandas, NumPy, memory-mapping

Лекция 1 - изображение номер двадцать восемь
Лекция 1 — изображение номер двадцать восемь

Python часто используется для анализа данных, обработки больших таблиц, массивов. Производительность при этом сильно зависит от того, используем ли мы оптимизированные инструменты вроде NumPy/Pandas или пытаемся обрабатывать данные чисто на Python. Рассмотрим ключевые моменты:

• Векторизация с NumPy: NumPy – библиотека для работы с массивами, реализованная на C. Она позволяет выполнять операции над целыми массивами без явных Python-циклов. Например, прибавление двух массивов поэлементно выполнится внутренним высокопроизводительным кодом. Выигрыш: на больших массивах NumPy может быть в несколько сотен раз быстрее Python-циклов 50. Конкретный пример: вычисление экспоненты для миллиона чисел – Python-цикл с занял ~646 мс, а NumPy- векторизация (arr) – ~20 мс 50 51. Разница ~30 раз. Чем больше массив – тем сильнее эффект (часто говорит о 50-1000x ускорении). Причина – NumPy написан на C и обрабатывает данные в непрерывной памяти, используя SIMD-инструкции, оптимизации и т.д., в то время как Python-цикл каждый раз тратит ресурсы на интерпретацию.

Правило: Если вы обрабатываете большие числовые массивы или матрицы – используйте NumPy вместо чистого Python. Например, нужно умножить каждый элемент списка на 2: [x*2 for x in list] – ок, но если list очень большой (миллионы элементов), лучше преобразовать его в и сделать array * 2. Особенно выигрывает NumPy на операциях линейной алгебры: матричное умножение, трансформации, суммирование, статистика – всё сделано с учётом производительности.

Память: NumPy также значительно экономит память по сравнению с Python-списками. Один элемент float в Python – объект (~24 байта структуры + 8 байт значение и накладные). В NumPy float64 – ровно 8 байт. На миллионах элементов это огромное различие. Меньше память – больше помещается в кэш процессора – быстрее операции.

Batch-обработка: С NumPy следует стремиться формулировать операции над целыми массивами, а не писать циклы. Даже если нужна условная логика, часто можно использовать булевы маски или функции. Например, отфильтровать массив: вместо Python- списка comprehension [x for x in arr if cond(x)], вы делаете mask = cond(arr) (это булев массив) и потом filtered = arr[mask] – всё внутри C.

  • Pandas для табличных данных: Pandas построен на NumPy и предоставляет высокоуровневые структуры DataFrame и Series для работы с таблицами (аналог DataFrame R или таблиц SQL). Pandas очень удобен, но надо знать, как правильно его использовать для производительности:
  • Использовать векторизированные методы Pandas: Это те, которые оперируют сразунад колонками. Например, df[‘new’] = df[‘col1’] + df[‘col2’] – выполнитсябыстро (под капотом NumPy). А вот (custom_func, axis=1) – для каждойстроки вызывает Python-функцию, то есть по сути скрытый цикл на Python, что медленно 52.
  • Избегать () и apply по строкам: Как показали бенчмарки, итерация по DataFrame строка за строкой – очень медленно (по сути, itertuples/iterrows достают Python-объекты). В одном эксперименте проход по DataFrame с 1 млн строк: iterrows ~3.66 сек, apply ~0.404 сек, а векторизированное вычисление – 0.0104 сек 53 54 – улучшение в 350 раз. Это яркий пример: код, который в Pandas выглядит короче, но всё равно вызывает Python-функцию на каждую строку (apply), всё ещё далек от потенциала. Лучше найти способ сделать то же средствами Pandas/NumPy без Python-циклов. Если уж нужно сложную логику на каждую строку – возможно, стоит использовать Cython или Numba (Pandas умеет apply с numba via (func, engine=’numba’)).
  • Типы данных Pandas: Чтобы DataFrame работал оптимально, нужно использовать правильные типы. Например, если колонка содержит категории (строки повторяются) – лучше преобразовать её в category dtype. Это не только сэкономит память, но и ускорит операции группировки/сравнения на этой колонке. Если у вас много мелких целых – можно явно задать dtype=’int8′ или int32 вместо стандартного int64, опять же сэкономив память (и снизив нагрузку на кэш).
  • Методы Pandas vs Python: Pandas предоставляет много методов: группировки(().agg()), сортировки, слияния, фильтрации (()). Они реализованы эффективно. Старайтесь ими пользоваться вместо того, чтобы выгружать данные в Python и там обрабатывать. Например, вам нужно отфильтровать строки по какому-то сложному условию – можно сделать df[(df[‘A’]>5) & (df[‘B’].isin(values))] – это вся фильтрация на C/NumPy уровнях. Если попытаться то же через цикл по () – будет в десятки-сотни раз медленнее.
  • Применение.loc/.iloc правильно: Если нужно итеративно присваивать – лучше собрать данные в NumPy массив, потом присвоить столбец целиком, чем присваивать по одной ячейке (каждое присваивание – отдельное действие с проверками). В идеале, старайтесь избегать побайтовой обработки DataFrame в Python – используйте поблочную.

•Масштабирование на большие данные (out-of-core): Pandas и NumPy работают в памяти, поэтому при данных больше RAM возникают проблемы. Пара подходов:

  • Чтение/запись с помощью итераторов: pandas.read_csv имеет параметр chunksize – можно читать огромный CSV кусками (например, по 100k строк),обрабатывать каждую порцию и освобождать. Это предотвратит взрыв памяти и можетраспараллеливаться (см. Dask).
  • Библиотеки для out-of-core: Dask DataFrame – позволяет работать с DataFrame,превышающим память, распределяя на кластер или на диск (аналог Pandas, но ленивый). Vaex – для очень больших наборов данных (размеры порядка гигабайт до терабайт) с использованием memory-mapping и колонко-ориентированного хранения. Polars (на Rust) – новый df-движок, очень быстрый и может работать лениво.
  • Memory mapping больших массивов: Уже упоминали – чтобы работать с массивом, больше памяти, с приемлемой скоростью. Если у вас, скажем, 10GB бинарный массив – позволит взять его с диска частями.
  • Базы данных/SQL: Когда данные совсем велики или более структурированы, иногда лучше переложить часть работы на СУБД (SQL запросы, агрегаты) или специализированные хранилища (ClickHouse для аналитики) вместо Python. Это не про Python-оптимизацию, но про общую производительность системы.
  • Parallel computing для NumPy/Pandas: По умолчанию, операции над NumPy массивом выполняются в одном потоке (за исключением BLAS, который может многопоточно умножать матрицы). Если у вас 8 ядер и вы хотите ускорить, можно использовать:
  • NumExpr: библиотека, которая разбирает строковые выражения типа “sqrt(a**2 + b**2)” и вычисляет их эффективно и многопоточно, часто быстрее NumPy.
  • Parallelizing: Dask или Joblib могут распараллелить операции, разбив массив/DF на части. В Pandas нет встроенного multi-threading, но Dask DataFrame, modin, swifter – попытки сделать Pandas parallel. Их эффективность зависит от задачи.
  • Vectorized libraries: Если работа с массивами включает линалг – подключение MKL (Intel Math Kernel) или OpenBLAS (они умеют использовать все ядра для больших матриц).Резюме: «Работа с большими данными на Python должна опираться на оптимизированные низкоуровневые библиотеки (C/C++). Python-код должен только оркестрировать эти вызовы, а не перебирать элементы сам.»Пример: нужно вычислить сумму элементов двух очень длинных списков. На Python: total = 0 for x, y in zip(list1, list2): total += x + yЭто крайне медленно для миллионных списков. На NumPy: import numpy as np a = (list1, dtype=np.float64) b = (list2, dtype=np.float64) total = (a + b)

Здесь практически всё происходит на C – создание массивов (O(n), но С и без overhead на объект каждый элемент), сложение (C-loop), суммирование (может использовать SIMD). На практике, первый вариант с 10 млн элементов может быть на порядок-два медленнее второго.

Иллюстрация ускорения Pandas: Рассмотрим задачу: есть DataFrame с миллионом записей о транзакциях, нужно вычислить колонку “reward” по сложному правилу (как в примере). Цикл через iterrows: 3.66s, apply: 0.404s, векторизация: 0.0104s 53 54. Разница между наивным и оптимальным подходом – 350х. Это демонстрирует, насколько важно избегать чистого Python при обработке больших объемов: код с apply на Pandas в 9 раз быстрее явного цикла, и ещё в 40 раз медленнее,чемвекторизированноерешение 55 56.Тоесть,дажевродебы”оптимизированный”с точки зрения Python метод (apply) всё равно на порядок медленнее возможного – надо идти до конца и задействовать весь потенциал.

• Memory-mapping (подробнее): Pandas может читать данные из memory-mapped файла (например, Feather или Parquet формат, частично). даёт ndarray с ленивой загрузкой. Если, скажем, у вас есть большой бинарный массив на диске (например, выгрузка датчиков), вместо (который читает целиком), можно использовать memmap – работать будет, как с массивом (вы можете брать срезы, и они подгрузятся по требованию). Python-speed блог отмечал, что memory-mapping удобен, но есть альтернатива – форматы как Zarr или HDF5, которые разбивают данные на чанки и позволяют загружать фрагментами. Это скорее вопрос выбора инструмента (иногда лучше сразу хранить в HDF5 с компрессией и вытаскивать надо – Pandas умеет read_hdf с фильтрами, etc).

Заключение: Для больших данных используйте “heavy lifting” библиотеки. Наглядно: «операции NumPy могут выполняться на порядки быстрее аналогичных на чистом Python, за счёт использования быстрого низкоуровневого кода и эффективного доступа к памяти» 57. Pandas при правильном подходе даёт удобство не в ущерб скорости (но требует знаний, где подстерегают ловушки вроде apply).

И последнее: следите за потреблением памяти.

⚡ - изображение номер двадцать девять
⚡ — изображение номер двадцать девять

В больших задачах упереться можно не только в CPU, но и в память (swap убьет производительность). Поэтому оптимизация памяти (правильные типы, удаление ненужных объектов, del DataFrame после использования, использование генераторов вместо списков где применимо) – тоже часть работы с big data. Это не ускорение CPU, но предотвращение деградации скорости из-за memory pressure.

Компиляция и упаковка: PyInstaller, Nuitka, zipapps

Как скомпилировать python в exe - изображение номер тридцать
Как скомпилировать python в exe — изображение номер тридцать

Когда речь идет о деплое приложений или о желании получить дополнительный прирост скорости за счет компиляции – на помощь приходят специальные инструменты:

• PyInstaller: утилита, которая собирает ваше Python-приложение (скрипт и все зависимости) в самостоятельный исполняемый файл (или папку) с вложенным интерпретатором. Это в первую очередь про удобство распространения: вы можете выдать клиенту один.exe файл вместо набора.py и установки интерпретатора. С точки зрения производительности, PyInstaller не делает ваш код машинным – внутри exe все равно работает CPython и ваши.pyc. Теоретически, небольшое ускорение старта программы возможно, так как модули можно загружать из единого zip-архива (меньше операций диска). Но существенного прироста времени исполнения не ждите – скорость будет как у обычного CPython. PyInstaller имеет опцию оптимизации (–onefile – optimize=2), которая убирает debug-информацию и т.п., но это мало влияет. Основное – удобство.

Производительность: zipapp в целом не ускоряет код. При первом запуске, Python импортирует модули из zip (zipimport). Это может быть чуть медленнее, чем с диска, особенно если zip сжат (он по умолчанию может сжимать). В трекере CPython отмечали, что zipimport на больших архивах немного тормозной 58 (50 мс на 1000 файлов архива). Но это цифры в масштабе инициализации. В runtime разницы нет – код исполняется так же. Если хотите уменьшить overhead zipimport, можно создать zip с –compresslevel 0 (не сжимать, только упаковка) – тогда чтение модулей не требует распаковки.

Когда использовать zipapp: Для деплоя утилит внутри одной организации, где Python точно есть. Например, вы пишете скрипт, которым будут пользоваться админы на разных машинах – вместо распространять папку с кодом, делаете.pyz. Его удобно копировать, версия контролируется.

• Nuitka: Это уже настоящий компилятор Python в С/C++. Nuitka берет ваш код (и все зависимости), транслирует в C++ код, который внутри вызывает CPython API. То есть, он по сути компилирует ваш скрипт в эквивалент расширения. Затем компилирует этот C++ (нужен компилятор, например gcc) и линковает с libpython. Получается исполняемый файл или библиотека. Nuitka добивается полной совместимости – поддерживает практически все, включая динамические особенности Python.

Производительность: Nuitka обычно даёт умеренный прирост скорости. К примеру, автор Nuitka упоминал, что целевые оптимизации могут дать ускорение 2x на общем приложении 38. Пользователи тоже отмечают, что «Nuitka не бывает медленнее CPython, иногда в 2-4 раза быстрее на отдельных функциях, но редко более 20% по всей программе» 59. То есть, ожидать 10-кратных ускорений не стоит. Прирост идёт за счёт оптимизации вызовов, устранения интерпретатора на некоторые вещи, но GIL никуда не делся – это все та же семантика CPython.

Плюсы Nuitka: – Получаете нативный exe (как PyInstaller, но без отдельного интерпретатора – он встроен). – Код частично защищен (обратно в.py его так просто не получить, хотя при желании можно дизассемблировать). – Есть некоторые оптимизации: Nuitka может, например, выявлять функции, которые не используют динамические возможности, и оптимизировать их. Со временем, возможно, Nuitka станет более оптимизирующим (как Cython с типами).

Минусы: – Компиляция может быть долго (большие проекты – минуты, десятки минут). – Экешей: exe размером больше, чем исходники (включает runtime). – Не все пакеты поддерживаются out-of- the-box (но большинство работают).

Когда использовать: Если нужно скрыть исходный код (до некоторой степени) или чуть ускорить определённый сценарий без переписывания. Например, у вас скрипт выполнения арифметики, и нужно выжать максимум, но переписывать на C нет времени – Nuitka может дать 2x (как один пользователь заметил – “маленькие проекты ускоряет ~2x, мне хватило” 38). Также Nuitka часто применяют вместе с PyInstaller: сначала Nuitka-компилируют части кода (для оптимизации и обфускации), затем все собирают PyInstaller в один exe.

• Cython как упаковка: Мы упоминали Cython в контексте оптимизации. Стоит добавить, что Cython можно использовать, чтобы превратить Python-проект в расширение (pyd/so) и импортировать, или даже вызвать PyInit___main__ чтобы сделать исполняемый. Но чаще Cython применяют для отдельных модулей, а сборка экзешника – это уже PyInstaller + Cython.

Архитектурные моменты

10 - изображение номер тридцать один
10 — изображение номер тридцать один

Архитектурные моменты: В серверных (backend) приложениях часто вместо объединения в exe, используют контейнеры (Docker) – туда кладут Python окружение и код. Здесь PyInstaller не особо нужен. PyInstaller скорее для десктоп- и утилит.

Вывод: – PyInstaller/zipapp – packing tools, удобство деплоя, не ускорители. – Nuitka – compiling tool, умеренный ускоритель, плюс обфускация, возможно стоит пробовать если всё остальное оптимизировано. – Cython/PyPy/Numba – более эффективны для оптимизации, но требуют либо изменения кода (Cython/Numba) либо замены интерпретатора (PyPy).

Если целью стоит именно ускорение, с минимальными изменениями – PyPy или Nuitka. PyPy может 4x ускорить, Nuitka ~2x, но PyPy требует убедиться, что всё совместимо. Nuitka – требует длительной компиляции, но потом плод – обычный exe (можно запускать без Python).

К слову, Guido van Rossum рекомендовал: “If you want your code to run faster, you should probably just use PyPy.” 61 – это шутливо, но PyPy реально часто даёт больший выигрыш, чем статическая компиляция Nuitka. Однако PyPy – не packaging solution, а runtime.

Пример сравнения: В статье на сравнили CPython, PyInstaller, Nuitka, PyPy, Cython на бенчмарке pystone 62 35: – CPython ~ 600k pystones/sec (это относительная метрика). – Nuitka скомпилированный ~600k (примерно то же, иногда чуть выше, у них вышло 597k vs 610k) 63 35. – PyPy ~1.77 million pystones/sec 36 – почти 3х ускорение. – Cython (без специальных оптимизаций) ~228k (даже медленнее, т.к. не добавили типы, пример показал важность правильного использования Cython) 64. Вывод: PyPy был самым быстрым на том тесте. Nuitka просто не замедлил относительно CPython, но и не дал чуда. Cython без типов – тоже. С типами, конечно, Cython мог бы выиграть, но потребовалось прописать их.

Итого: Packaging (PyInstaller, zipapp) – вещь для удобства, не делайте на них ставку ради скорости. Компиляция (Nuitka) – может слегка помочь, но скорее полезна для deploy и защиты кода. Если уже всё профилировано, алгоритмы оптимальны, GIL мешает – Nuitka не поможет (GIL останется). Тогда уже либо multiprocessing распараллеливать, либо думать о C-реализации критических частей.

Общие советы по пишущемуся быстрому и поддерживаемому коду

Продвинутые методы улучшения кода на - изображение номер тридцать два
Продвинутые методы улучшения кода на — изображение номер тридцать два

Наконец, обсудим общие принципы, которые помогают сделать код и быстрым, и поддерживаемым:

• Профилируй и измеряй перед оптимизацией: Золотое правило: «Преждевременная оптимизация – корень всех зол» (Д. Кнут) 65 1. Не стоит гадать, какой код медленный – лучше замерить (вплоть до простого print(time() – start) или использовать профилировщики). Оптимизируйте узкие места, а не всё подряд. Это сэкономит время и убережет от усложнения программы без нужды. Например, можно тюнить цикл, добиваясь 5% выигрыша, а оказывается, 80% времени программа ждёт ответа от базы – т.е. оптимизировать надо слой работы с БД или параллелить запросы.

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

Проверить принадлежность через список vs через set – разница колоссальна при росте данных. Использование сортировки vs квадратичного алгоритма. Всегда оценивайте Big O своих решений. Улучшение сложности с O(n^2) до O(n log n) может ускорить на больших n на порядки, в то время как микротюнинг O(n^2) ничего принципиально не даст. Грамотная архитектура и алгоритмы – фундамент быстрого кода. И наоборот, неудачная структура (например, хранить данные в списке и при каждом запросе линейно искать) трудно компенсируется низкоуровневыми оптимизациями.

•Пишите код понятно (PEP8, чистый стиль), затем оптимизируйте локально: Читабельность – ключ к поддержке. Как отмечалось: «код пишется для людей, а не для машин. Если понадобится ускорить – понятный код легче оптимизировать, чем запутанный» 30. Поэтому сперва реализуйте корректно и понятно. Соблюдайте PEP8 (отступы, имена, организация кода) – это упрощает понимание командой, а значит, и скорость, с которой можно внести улучшения. Не пишите микрооптимизированный, но нечитаемый код, если на то нет крайней необходимости. Например, есть известный приём в Python: использовать трюки с побитовыми операциями для ускорения, но они усложняют код. Применять такое стоит только если действительно нужно и документировать.

  • Локализуйте оптимизированный (менее понятный) код: Если вам пришлось написать что-то хитрое для скорости – изолируйте это в функцию/метод с хорошим именем и комментарием. Тогда основная логика останется чистой, а подробности спрячутся. В комментариях укажите, почему тут такая сложность: “Оптимизация: используем битовые флаги вместо списка для экономии памяти и ускорения доступа”. Будущие разработчики (возможно, вы сами через полгода) должны понимать, что происходит, иначе велика вероятность, что при первом рефакторинге ваш “хитрый” код сломают или выбросят.
  • Документируйте и тестируйте функциональность до оптимизации: Убедитесь, что есть тесты. Оптимизация часто связана с усложнением кода – автоматические тесты помогут убедиться, что вы ничего не сломали. Особенно это актуально при вводе параллелизма (можно легко внести гонку) или кеширования (можно получить несвежие данные). Тесты плюс типизация (PEP484 type hints) – очень помогают поддерживать оптимизированный код корректным.
  • Учитесь у стандартных средств: Многие оптимизации уже воплощены в стандартной библиотеке. Читайте код модулей itertools, functools, collections – там можно найти интересные приёмы. Например, как реализован lru_cache или deque – это может дать идеи для ваших специфических задач.
  • Оценивайте цену оптимизации: Иногда усложнение кода ради micro-оптимизации не стоит того. Например, расклеивать выражение на несколько строк, использовать низкоуровневые C-расширения – всё это увеличивает стоимость поддержки. Если выигрыш – миллисекунды, а цена – дни работы и риск багов, может, не нужно. Как сказано на Reddit: “Большинство программ не ограничены CPU. Оптимизация вычислений часто бессмысленна, если пользователь ждёт I/O” 66. То есть если ваш веб-сервис проводит 99% времени в ожидании БД, нет смысла тюнить остальной код, лучше сфокусироваться на ускорении запросов к БД или кэшировании.
  • Выбирайте современные версии Python и библиотек: Каждый новый релиз Python приносит оптимизации. Python 3.11, например, около 25% быстрее 3.10 в среднем 67. Всего лишь обновившись, вы получите ускорение “бесплатно”. То же по библиотекам: например, Pandas 2.0 принес ускорения на группировках, NumPy новые версии иногда
  • оптимизируют операции. Следите за релиз-нотами. Конечно, обновление требует тестирования, но в плане производительности это одна из самых простых мер.
  • Оптимизация архитектуры приложения: На высоком уровне, подумайте, можно ли ускорить за счет изменения архитектуры:
  • Распараллелить на уровне процессов/серверов (scale out).
  • Избежать лишних операций (кеширование, как мы обсуждали, или отказ от ненужныхфункций).
  • Разделить тяжелые и легкие задачи (например, вынести обработку больших данных воффлайн-джобу, а в онлайн-пути использовать заранее посчитанное).
  • Использовать подходящие технологии: например, вместо генерировать отчет по запросу –подготовить его асинхронно и хранить готовым.
  • В веб-архитектуре: может добавить CDN, чтобы не генерировать часто запрашиваемыестраницы, а отдавать кешированные.Все эти вещи иногда дают больше выигрыша, чем низкоуровневый оптимизатор может мечтать.•PEP8 – не в ущерб скорости: Хотя PEP8 – о стиле, он опосредованно влияет на производительность разработки (а отлаженность кода тоже часть перфоманса проекта!). Например, именование – если переменные названы ясно, меньше шансов на ошибки и неправильные предположения, которые могут привести к неэффективному коду. Структура модулей – PEP8 советует разбивать большой файл на логические части (но не делает жестко) – так удобнее профилировать и оптимизировать куски. Одним словом, поддерживаемость кода помогает оптимизации, потому что вы быстрее разберетесь, что улучшать.
  • Используйте инструменты для диагностики производительности: Помимо профилировщиков, полезны трассировка (logging времени выполнения ключевых этапов), мониторинг (в веб-приложении – APM, метрики). Это дает знание, где узкие места проявляются в продакшене. Иногда предположения на дев-машине не совпадают с реальностью нагрузки. Например, профилировали вы на небольшом наборе – а на бою всплыли проблемы с аллокацией памяти на больших данных (GC паузы, и т.д.). Мониторинг (графики времени отклика, CPU, memory) – подскажет, куда копать.
  • Не пренебрегайте сборкой мусора и памятью: Python – сборщик мусора (GC). Если ваш код создает огромное количество мелких объектов, GC может тратить ощутимое время. Оптимизируйте количество объектов: например, вместо создавать миллионы кортежей, возможно, лучше использовать массив NumPy. Или использовать слоты в классах, чтобы уменьшить overhead. Хотя CPython GC неплох, но бывают патологии (например, много короткоживущих объектов в цикле – нагрузка на аллокатор). Инструмент tracemalloc может помочь отследить рост памяти. Чистый код с меньше лишними объектами – быстрее.
  • Вопросы безопасности и корректности: Иногда “оптимизированный” код может иметь побочные эффекты. Например, использовать для экономии памяти – но забыть, что iterator расходуется. Или кеширование – но не продумать invalidation, и получить баг “пользователь видит чужие данные”. Поэтому всегда проверяйте, что оптимизация не нарушила функциональность. Безопасность: если вы, ради скорости, отключаете проверки (например, assert убрали, или Cython с boundscheck=False –
  • следите, чтобы это не открывало уязвимостей (в Cython может привести к segfault при неправильных индексах, но это уже не секьюрити, а стабильность).

•Здравый смысл и баланс: Помните, что цель – не самый быстрый в мире код, а достаточно быстрый для требований, при этом надежный и понятный. Бывает, что “достаточно быстрый” достигается малыми правками (кеширование, SQL-оптимизация), и не нужно увлекаться дальнейшим ускорением. В других случаях – требования жесткие, и тогда да, приходится жертвовать простотой (переписывать на C++/Rust). Оценивайте ROI (окупаемость) оптимизации.

Наглядная мысль: «Читаемый, но медленный код гораздо легче сделать быстрым, чем быстрый, но нечитаемый – сделать правильным» 30. Так что придерживайтесь прогрессивного улучшения: пишем чисто -> профилируем -> точечно ускоряем -> профилируем снова -> рефакторим, если надо.

И напоследок, не забывайте об опыте сообщества: есть бесчисленное множество статей, дискуссий о Python performance. Изучайте успешные кейсы, новые инструменты (тот же PyPy, Numba – когда-то о них многие не знали и мучились). Python – язык с сильным сообществом, и практически для любой проблемы производительности уже может быть найдено решение или библиотека.

Следуя этим главам и принципам, можно добиться от Python-кода максимума производительности, сохранив при этом его высокую продуктивность разработки и сопровождения. Быстрых вам программ!

1. Python Profiling tools (cProfile, timeit) – Analytics Vidhya 2. Medium – 5 Python profiling tools 4 53. Princeton Research – Line_profiler usage 8 94. Python Wiki – TimeComplexity of data structures 13 12 5. Bisect documentation – dict vs binary search 14

9. GFG – List comprehension vs loop performance 2810. Jake VanderPlas blog – Numba vs pure Python speedup 3111. Reddit r/Python – PyPy vs CPython discussion 38 5912. – Threading vs Multiprocessing vs Asyncio 40 4313. – aiohttp performance for high concurrency 4514. BrightData blog – aiohttp 10x faster than requests 4215. Cameron MacLeod – lru_cache example (Flask route) 46 4716. Redis cache benefits – BlackSlate blog 4917. StackOverflow – NumPy vectorization vs loop (646ms vs 20ms) 50 51 18. Exxactcorp blog – Pandas loop vs apply vs vectorize (350x gain) 53 54 19. Hacker News – Nuitka ~2x vs CPython, PyPy ~3x 38 5920. Reddit – Readability vs performance advice 30 1

Кейс для ускорения кода на Python

Анализ и ускорение - изображение номер тридцать три
Анализ и ускорение — изображение номер тридцать три

Рассмотрим простую функцию, которая вычисляет сумму квадратов элементов большого списка чисел.

В этом примере мы используем стандартный подход с помощью встроенной функции sum и генератора списка

А теперь применим техники ускорения кода и понаблюдаем, как каждая из них отразится на скорости выполнения программы.

Медленный способ

Ускоряем - изображение номер тридцать пять
Ускоряем — изображение номер тридцать пять

result = «» for word in words: result += word + » » # создаёт новый объект каждый раз

Сравнение

Speed of - изображение номер тридцать шесть
Speed of — изображение номер тридцать шесть

# Медленно squares = [] for x in range(1000): (x**2) # Быстро squares = [x**2 for x in range(1000)]

List comprehension компилируется в более эффективный байт-код. Подробнее о list comprehensions и других python фишках — в статье редкие трюки python.

Результаты

Применение машинного обучения для решения задач в - изображение номер тридцать семь
Применение машинного обучения для решения задач в — изображение номер тридцать семь

Таблица №1

Время выполнения каждого подхода будет зависеть от машины и конкретного случая, но ориентировочно можно представить следующие результаты
стандартный подход: 0 25 секунды;
map и lambda: 0 18 секунды;
list comprehension: 0 17 секунды;
NumPy: 0 006 секунды;
Pandas: 0 006 секунды.

Как видно, оптимизация может значительно ускорить код на Python. Однако не стоит забывать, что оптимальность любого подхода зависит от конкретного случая, и не всегда нужно стремиться к максимальной скорости исполнения в ущерб читаемости и поддерживаемости (list comprehension, внутри которого ещё два list comprehension, map и лямбда-функции — и всё это в одну строку, конечно, ускорит код, но прочитать его будет сложно).

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

Не копируйте данные без необходимости

Ускорил - изображение номер тридцать восемь
Ускорил — изображение номер тридцать восемь

# Вместо big_list[:] = [x for x in big_list if x > 0] # Лучше — фильтр «на лету» filtered = (x for x in big_list if x > 0)

Избегайте глубокой вложенности в циклах

Если внутри цикла — ещё цикл — и ещё один — код не только медленный, но и не читаемый. Выносите логику в функции:

for user in users: if is_active(user): send_report(user) # внутри — своя проверка данных

Часто задаваемые вопросы об ускорении Python-кода

Вопрос: Какой самый простой способ ускорить Python-код?
Ответ: Используйте встроенные функции и библиотеки (например, NumPy) вместо ручных циклов.

Вопрос: Что такое профилирование и зачем оно нужно?
Ответ: Профилирование помогает найти узкие места в коде, которые замедляют выполнение.

Вопрос: Когда стоит использовать Numba или Cython?
Ответ: Для численных расчетов и тяжелых циклов, где стандартный Python медлителен.

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

Вопрос: Что такое векторизация?
Ответ: Это применение операций ко всему массиву данных сразу, без явных циклов (например, через NumPy).

Вопрос: Помогает ли многопоточность в Python?
Ответ: Для I/O-задач — да, для CPU-задач — лучше использовать многопроцессность из-за GIL.

Вопрос: Какой алгоритм выбрать для ускорения?
Ответ: Всегда оценивайте сложность: O(n log n) быстрее O(n^2) для больших данных.

Вопрос: Стоит ли использовать PyPy вместо CPython?
Ответ: Да, для чистого Python-кода PyPy может дать значительный прирост скорости.

Вопрос: Как уменьшить потребление памяти?
Ответ: Используйте генераторы вместо списков, освобождайте память вручную и избегайте копирования данных.

Вопрос: Что такое короткое замыкание в условиях?
Ответ: Это когда Python прекращает вычисление логического выражения после получения первого определенного результата.