Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → NumPy для разработчика: массивы, broadcasting, память

NumPy для разработчика: как думать массивами, а не циклами

Практическое введение в NumPy для backend-разработчика: ndarray и память, broadcasting, индексация масками, оси операций — правила, которые работают в pandas и за его пределами.

Прямой ответ

NumPy стоит понимать не как «библиотеку для математики», а как модель данных, на которой стоит половина Python-экосистемы: pandas-колонки, тензоры, эмбеддинги, картинки — всё это ndarray или ведёт себя похоже. Три идеи покрывают 90% практики: однородный непрерывный массив (один тип, быстрая арифметика в C без интерпретатора), broadcasting (операции между массивами разной формы по явным правилам) и индексация масками (выбор элементов условием без циклов). Освоив их, вы перестанете писать for-циклы над данными и начнёте писать формулы над массивами.

Модель: ndarray — это не список списков

np.array хранит данные одним непрерывным блоком одного типа (dtype: int64, float32, bool...) с метаданными формы (shape) и шагов (strides). Отсюда три следствия, которые отличают его от списка Python. Первое — арифметика работает поэлементно и векторизована: arr * 2 + 1 вычисляется одним C-циклом, в десятки раз быстрее list comprehension. Второе — память компактна и предсказуема: arr.nbytes говорит точный размер; float32-массив вдвое меньше float64, что на больших данных решает. Третье — операции создают новые массивы (копии), а срезы-views разделяют память с оригиналом: изменение view меняет исходный массив — источник классических багов, лечится осознанным .copy().

Broadcasting: правила, а не магия

Broadcasting — механизм, позволяющий оперировать массивами разной формы: размеры сравниваются справа налево, и размер совместим, если равен или равен 1 (единица «растягивается»). Примеры, которые встречаются каждый день: вектор-строка плюс матрица (прибавить средние по колонкам), колонка на вектор коэффициентов, булево сравнение массива со скаляром. Правило практики: если broadcasting «не сработал», shapes просто несовместимы — и почти всегда помогает явный reshape(-1, 1) (колонка) вместо reshape(-1) (строка); разница между формами (n,) и (n,1) — самая частая причина непонятных ошибок. Читаемость выигрывает от явных имён осей в комментариях: # (users, features) * (features,).

Индексация: маски вместо фильтров

Булева индексация — главный инструмент: arr[arr > 0], data[(data[:, 0] > 5) & (data[:, 1] < 10)] — выбор без единого цикла, и именно эта идиома переносится в pandas (df[df['a'] > 0]). Фантазийная индексация списком индексов (arr[[3, 1, 7]]) — произвольная перестановка/выборка; срезы — дешёвые views. Полезные идиомы: np.where(cond, a, b) (тернарник поэлементно), np.argsort (порядок сортировки — для перестановки нескольких массивов согласованно), arr[cond] = value (массовая правка по условию). Антипаттерн — изменение массива во время итерации по нему или по его маске: результат неопределённый, как и в обычном Python, но молчаливее.

Оси: сердце многомерных операций

Агрегации принимают axis, и путаница в осях — главный мысленный барьер. Мнемоника, которая работает: axis=0 — «вдоль строк» (коллапс строк, результат по колонкам), axis=1 — вдоль колонок; для 3D проще думать «какую ось убираем». Проверкаshapes до/после (print(arr.shape)) в первые недели обязательна — потом формы начинают «чувствоваться». Сюда же: keepdims=True сохраняет размерность и делает результат совместимым для broadcasting (прибавить среднее по колонкам обратно — классика).

Где это встречается вне «математики»

Backend-разработчику NumPy нужен чаще, чем кажется: парсинг бинарных протоколов (np.frombuffer — разбор байтов структур без struct-петель), работа с изображениями (картинка — массив HxWx3), эмбеддинги и косинусная близость (одно скалярное произведение), тепловые карты и матричные вычисления в логике сервиса, а главное — pandas: любая «недостающая» операция pandas почти всегда выражается через её .to_numpy() и обратно. Умение прочитать документацию numpy-функции (типы входа/выхода, поведение на осях) — навык, который окупается во всём стеке данных; типизация же numpy-кода — отдельная дисциплина, о ней в статье про типизацию и mypy.

Производительность: когда NumPy, а когда выше

Честная лестница выбора: малые данные → достаточно чистого Python; табличные данные → pandas поверх NumPy (эргономика важнее микросекунд); горячие числовые циклы → numpy-выражения, а если не выражается — numba; по-настоящему большие объёмы → специализированные движки. Замер обязателен: timeit на реальных размерах, и помните про накладные расходы создания маленьких массивов — numpy выигрывает с тысяч элементов, а не десятков. Практика поиска узких мест — в статье про профилирование; приёмы векторизации на таблицах — в материале про производительность pandas.

Практический сценарий: косинусная близость эмбеддингов

Задача из жизни RAG-сервиса: найти ближайшие к запросу тексты среди десяти тысяч эмбеддингов размерности 768. Наивный путь — цикл по строкам с попарным скалярным произведением на чистом Python — идёт секунды. NumPy-путь: матрица E формы (10000, 768) — float32, вектор запроса q — (768,). Нормируем один раз при индексации: En = E / np.linalg.norm(E, axis=1, keepdims=True) — обратите внимание на keepdims=True (норма формы (10000, 1) корректно растягивается broadcasting'ом; без него формы разъедутся — та самая разница (n,) против (n, 1)). Поиск — одно выражение: sims = En @ q — матрично-векторное произведение, десять тысяч скалярных произведений одной BLAS-операцией, миллисекунды. Топ-5 — np.argsort(sims)[-5:][::-1] (порядок сортировки как индексы — идеома, работающая с любым числом массивов согласованно). Память под контролем: float32 против float64 — пятнадцать мегабайт вместо тридцати (nbytes скажет точно); на миллионах векторов это уже гигабайты и вопрос «влезет ли в память ноутбука». И последний шаг дисциплины — проверка формы на каждом этапе (E.shape, q.shape, sims.shape) в первые дни; потом формы начинают чувствоваться, и код читается как формула.

FAQ

Почему np.array быстрее списка даже для простого цикла сумм? Один непрерывный блок одного типа = предсказуемая память, SIMD-инструкции, отсутствие распаковки Python-объектов на элемент. Список хранит указатели на объекты, и каждый шаг требует работы интерпретатора.

Чем dtype=object опасен? Это «список в массиве»: векторизация не работает (элементы — Python-объекты), память рыхлая, операции медленные. object-массивы законны для строк переменной длины и вложенных структур, но их появление в числовом пайплайне — сигнал ошибки типов.

Копия или view — как проверить? arr.base is None → копия; непустой base → view на другой массив. Практически: срезы — views, фантазийная индексация и большинство операций — копии.

Нужен ли NumPy, если я пишу только на pandas? Да, минимально: понимание broadcasting и масок объясняет половину поведения pandas, а .to_numpy() регулярно спасает там, где pandas-эргономика заканчивается.

Источники: официальное руководство NumPy (beginners, broadcasting, indexing) — ссылки в шапке.