6.2. ndarray¶
ndarray – это тип, который хранит числовые данные в numpy. Он представляет собой две вещи в одной: единый упакованный блок данных и небольшой дескриптор перед этим блоком, который говорит, как его читать.
6.2.1. Внутри коробки¶
Блок данных хранит каждый элемент массива встык, без ничего лишнего между ними. Каждый элемент занимает одинаковое количество байт – один для массива значений uint8, два для uint16, четыре для float. Массив uint8 из 256 элементов – это ровно 256 байт данных; те же 256 чисел в Python list занимают килобайт – по одному 32-битному слоту на элемент независимо от того, как мало бит на самом деле нужно значению.
Дескриптор записывает, что означает блок. Пяти значений достаточно, чтобы описать любой прямоугольный массив, независимо от количества измерений:
dtype– тип элемента. Определяет, сколько байт занимает каждый элемент и какой диапазон значений он может хранить (рассмотрено на странице Dtypes).itemsize– ширина в байтах одного элемента, выведенная из dtype.ndim– количество измерений (1 для вектора, 2 для матрицы, 3 для объёма, до 4).shape– размер вдоль каждого измерения в виде кортежа.strides– как шагать по блоку данных, чтобы обойти каждую ось. Рассмотрено на странице Форма и шаги (strides).
Вот и всё. Каждый быстрый путь в numpy – арифметика, редукции, broadcasting, нарезка – работает напрямую из этих пяти значений плюс указатель на данные, без поэлементных накладных расходов Python.
6.2.2. Что даёт этот дизайн¶
Из «упакованного блока + небольшого дескриптора» вытекают три свойства, которые определяют поведение остальной части раздела.
Поэлементная математика выполняется как один вызов. a + b между двумя массивами совпадающей формы складывает два буфера и записывает третий – всё внутри одного вызова библиотеки. np.sin(a) делает то же самое для синуса каждого элемента. Арифметические, сравнения и побитовые операторы все работают именно так.
Взгляд на те же данные иным способом бесплатен. Запрос подобласти массива или тех же данных, разложенных под другой формой, не перемещает ни одного байта. Операция возвращает новый дескриптор, указывающий на тот же блок данных. Результат называется представлением (view) – вторым окном в тот же базовый буфер. Запись через представление пишет в источник.
Смешанные формы всё равно работают. Более короткий массив против более длинного, строка против матрицы, столбец против строки – numpy выравнивает их с помощью broadcasting, небольшого набора правил, которые решают, какая короткая ось растягивается, чтобы соответствовать длинной. Растяжение виртуально; никакие данные не дублируются.
6.2.3. Чего стоит этот дизайн¶
Из того же дизайна следуют два ограничения.
Каждый элемент имеет один и тот же тип. Список может хранить int рядом с str рядом со списком из трёх других значений int; ndarray не может. Dtype фиксируется во время построения. Страница Dtypes рассматривает небольшой набор типов, которые поддерживает numpy, и правила, вытекающие из фиксации одного из них.
Рост массива не бесплатен. Список хранит запасные слоты в конце и дёшево поддерживает .append. ndarray имеет ровно тот размер, который ему нужен; добавление означало бы выделение нового, большего буфера и копирование в него старого содержимого. Метода append() нет, и это сделано намеренно. Правильный паттерн на камере – предварительно выделить приёмник в его окончательном размере и заполнить его; страница Производительность рассматривает эту технику.
С упакованным типизированным буфером для данных, небольшим дескриптором для метаданных и тремя поведенческими гарантиями (быстрая поэлементная математика, альтернативные представления тех же данных без копирования и формы, которые подвергаются broadcasting) ndarray является фундаментом, на котором покоится остальная часть главы. Как массив на самом деле появляется на свет – из литерала, из предзаполненного выделения, из буфера периферийного устройства – это следующий практический вопрос.