Вы когда-нибудь писали цикл в 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.
Векторизация: как писать код, который работает как 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 секунды. И вы пишете всего две строки.
Что использовать, когда?
Теперь вопрос: когда что применять?
| Ситуация | Решение | Ускорение |
|---|---|---|
| Операции над массивами чисел (сложение, умножение, тригонометрия) | 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 проще, быстрее и достаточно мощен.