ПроКодинг - Откроем для вас мир IT!

Вы когда-нибудь писали цикл в Python, который выполнялся часами? Или запускали скрипт, который на вашем ноутбуке тормозил, как будто он работает на компьютере 2005 года? Это не ваша вина - это просто Python по умолчанию медленный для тяжелых вычислений. Но есть выход. И он не в переходе на C++ или Rust. Он в правильном использовании NumPy, Numba и векторизации.

Почему обычные циклы в Python - это плохо

Python - язык для разработчиков, а не для процессоров. Когда вы пишете что-то вроде:

result = []
for i in range(1000000):
    result.append(i * 2 + 1)

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

Реальный пример: подсчет суммы квадратов чисел от 0 до 1 млн. На моем ноутбуке (Intel i7-1260P, 2026 г.) этот цикл занимает 1.8 секунды. Звучит не так уж плохо? Но когда вы делаете это в цикле 100 раз - например, в цикле обучения модели машинного обучения - вы получаете 3 минуты вместо пары секунд. Это не просто медленно - это неприемлемо.

NumPy: когда массивы заменяют циклы

NumPy - это не просто библиотека. Это другой язык внутри Python. Он работает с массивами данных в памяти как один непрерывный блок. И все операции над ними - на уровне C, без интерпретации.

Вот как выглядит та же задача с NumPy:

import numpy as np
arr = np.arange(1000000)
result = arr * 2 + 1

Время выполнения? 0.012 секунды. В 150 раз быстрее. Почему? Потому что NumPy не идет по каждому элементу - он говорит процессору: «выполни эту операцию над всеми 1 млн элементов сразу». Это называется векторизацией.

Векторизация - это когда вы пишете код, как будто работаете с целым массивом, а не с одним элементом за раз. NumPy умеет это. И это работает не только с умножением. Сложение, логарифмы, синусы, условные операции - всё это работает на массивах целиком.

Пример с реальной задачей: вычисление расстояний между 10 тыс. точками в 3D-пространстве. С циклами - 12 секунд. С NumPy - 0.08 секунд. Разница в 150 раз. И это только начало.

Что такое Numba и почему он меняет правила игры

NumPy - отличный инструмент, но он не спасает, если вы пишете сложную логику: условные операторы, циклы с выходом по условию, вызовы функций внутри цикла. Для этого нужен Numba.

Numba - это JIT-компилятор для Python. Он берет вашу функцию, анализирует её, и прямо во время выполнения превращает в машинный код, как будто это C-функция. И делает это с поддержкой многопоточности и векторизации.

Вот пример, который NumPy не может оптимизировать:

def compute_custom(x):
    result = 0
    for i in range(len(x)):
        if x[i] > 0.5:
            result += x[i] ** 2
        else:
            result += x[i] * 0.1
    return result

С обычным Python - 1.5 секунды. С Numba - всего 0.004 секунды. Да, вы не ослышались. 375 раз быстрее. И вы не меняете ни одной строки логики. Просто добавляете декоратор:

from numba import jit

@jit(nopython=True)
def compute_custom(x):
    result = 0
    for i in range(len(x)):
        if x[i] > 0.5:
            result += x[i] ** 2
        else:
            result += x[i] * 0.1
    return result

Numba не работает с любыми функциями. Он требует, чтобы код был «nopython» - то есть без использования объектов Python, словарей, списков, строк. Только массивы NumPy, числа, простые циклы. Но это не ограничение - это возможность. Вы переписываете логику, чтобы она стала компилируемой - и получаете скорость C с удобством Python.

Numba компилирует Python-функцию с циклами в машинный код, который обрабатывается процессором с высокой скоростью.

Векторизация: как писать код, который работает как GPU

Векторизация - это не фича NumPy. Это философия. Это когда вы перестаёте думать о «каждом элементе» и начинаете думать о «массиве как о целом».

Вот как вы должны думать:

  • Не: «пройти по каждому числу и проверить, больше ли оно 5»
  • А: «найти все числа больше 5 за один раз»

Пример: вычисление среднего значения только положительных чисел в массиве из 1 млн элементов.

Неправильно:

total = 0
count = 0
for val in data:
    if val > 0:
        total += val
        count += 1
avg = total / count

Правильно:

positive_vals = data[data > 0]
avg = np.mean(positive_vals)

Второй вариант - в 200 раз быстрее. И читается как английский текст. Это и есть сила векторизации. NumPy создает булев массив data > 0 - и использует его как маску. Всё происходит на уровне C-кода, без интерпретации.

Векторизация работает даже с многомерными массивами. Например, вычисление Евклидова расстояния между двумя матрицами 1000x1000. С циклами - 40 секунд. С NumPy - 0.03 секунды. И вы пишете всего две строки.

Что использовать, когда?

Теперь вопрос: когда что применять?

Сравнение подходов к оптимизации вычислений в Python
Ситуация Решение Ускорение
Операции над массивами чисел (сложение, умножение, тригонометрия) NumPy 50-200x
Сложная логика с циклами, условиями, функциями Numba (@jit) 100-500x
Работа с данными, которые нельзя векторизовать (например, строки, словари) Пока только Python -
Очень большие данные (больше 10 ГБ) NumPy + memmap или Dask Зависит от памяти

Если ваш код - это математика, и он работает с числами - NumPy. Если вы пишете сложный алгоритм, как, например, фильтрация сигналов или расчет траекторий - Numba. Если вы работаете с текстом, JSON или сложными объектами - тогда вы не сможете ускорить это сильно. Но это не значит, что вы не можете ускорить остальную часть кода.

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

Практические советы: как не попасть в ловушки

Несколько реальных ошибок, которые я видел сотни раз.

  • Не используйте списки для чисел. list в Python - это объекты. Каждый элемент - это указатель. NumPy работает с массивами фиксированного типа. Преобразуйте списки в np.array() как можно раньше.
  • Не растягивайте массивы в цикле. np.append() внутри цикла - это как покупать новый чемодан каждый раз, когда вы кладете в него вещь. Лучше заранее выделить массив нужного размера.
  • Не используйте for с range(len(array)). Это Python-стиль. В NumPy - np.ndenumerate() или маски.
  • Проверяйте типы данных. np.float32 вместо np.float64 может сэкономить 50% памяти и ускорить вычисления. Особенно важно при работе с большими данными.
  • Не забывайте про inplace операции. x *= 2 быстрее, чем x = x * 2, потому что не создает новый массив.

Один из самых мощных приемов - использовать np.einsum() для сложных операций с тензорами. Например, матричное умножение с перестановкой осей. Это может заменить 5 циклов и 30 строк кода на одну строку. И работать в 10 раз быстрее.

Когда не стоит оптимизировать

Оптимизация - это не цель. Это средство. Вы не должны оптимизировать всё. Вы должны оптимизировать то, что замедляет вашу систему.

Если ваша программа делает 1000 операций в секунду, и одна из них занимает 0.01 секунды - это 1% времени. Оптимизировать её - пустая трата времени. Найдите бутылочное горлышко. Используйте cProfile или line_profiler, чтобы увидеть, где реально тормозит код.

Часто самое медленное место - это не математика. Это чтение файла, сеть, база данных, или вызов внешней программы. Оптимизируйте сначала это. Потом - NumPy. Потом - Numba. И только потом - переход на C.

Заключение: скорость - это не магия

NumPy, Numba и векторизация - не «волшебные таблетки». Это инструменты, которые работают, если вы меняете способ мышления. Вы перестаёте писать код для интерпретатора - и начинаете писать для процессора.

Если вы работаете с данными, машинным обучением, научными расчетами - эти три инструмента не опциональны. Они обязательны. Без них вы тратите часы на то, что можно сделать за минуты. Или за секунды.

Начните с простого: возьмите самый медленный цикл в вашем коде. Замените его на NumPy. Запустите. Сравните. Потом попробуйте Numba. Вы удивитесь, насколько быстро может работать Python, когда вы перестаёте его «нагружать».

Какой из них быстрее: NumPy или Numba?

Numba быстрее, но только для сложных логик с циклами и условиями. NumPy быстрее для векторизованных операций - сложения, умножения, функций над массивами. Они не конкурируют - они дополняют друг друга. Используйте NumPy там, где можно, и Numba там, где NumPy не справляется.

Можно ли использовать Numba без NumPy?

Нет, неэффективно. Numba работает с массивами NumPy, потому что они хранят данные в памяти как непрерывный блок. Если вы используете обычные списки Python, Numba не сможет их оптимизировать. Всегда передавайте NumPy-массивы в Numba-функции.

Почему векторизация работает быстрее, чем циклы?

Потому что NumPy использует SIMD-инструкции процессора - они позволяют обрабатывать несколько чисел одновременно. Циклы в Python обрабатывают одно число за раз. Кроме того, NumPy не тратит время на проверку типов, вызовы функций и управление памятью для каждого элемента - всё делается одним пакетом на уровне C.

Какой тип данных выбрать: float32 или float64?

Для большинства задач в машинном обучении и научных расчетах достаточно float32. Он занимает вдвое меньше памяти и работает быстрее. Float64 нужен только если вы работаете с очень малыми числами (например, в физике) или когда требуется высокая точность при накоплении ошибок. В 90% случаев float32 - оптимальный выбор.

Стоит ли переходить на Cython вместо Numba?

Cython требует написания кода на языке, похожем на C, и компиляции. Numba - просто декоратор. Если вы хотите быстро протестировать ускорение - используйте Numba. Если вы пишете библиотеку, которую будете распространять, и вам нужна максимальная производительность - тогда Cython. Но для большинства задач Numba проще, быстрее и достаточно мощен.