Внутренняя организация массивов NumPy#
Полезно немного узнать о том, как массивы NumPy обрабатываются внутри, чтобы лучше понять NumPy. Этот раздел дает краткое объяснение. Подробности доступны в книге Трэвиса Олифанта Руководство по NumPy.
Массивы NumPy состоят из двух основных компонентов: необработанных данных массива (далее называемых буфером данных) и информации об этих необработанных данных. Буфер данных — это обычно то, что люди представляют себе как массивы в C или Fortran, непрерывный (и фиксированный) блок памяти, содержащий элементы данных фиксированного размера. NumPy также содержит значительный набор данных, описывающих, как интерпретировать данные в буфере данных. Эта дополнительная информация содержит (среди прочего):
Размер базового элемента данных в байтах.
Начало данных в буфере данных (смещение относительно начала буфера данных).
Количество измерения и размер каждого измерения.
Разделение между элементами для каждого измерения ( шагЭто не обязательно должно быть кратно размеру элемента.
Порядок байтов данных (который может не совпадать с нативным порядком байтов).
Является ли буфер доступным только для чтения.
Информация (через
dtypeобъект) об интерпретации базового элемента данных. Базовый элемент данных может быть простым, как int или float, или составным объектом (например, структуро-подобный), фиксированное символьное поле, или указатели на объекты Python.Следует ли интерпретировать массив как C-порядок или Порядок Fortran.
Такая организация позволяет очень гибко использовать массивы. Одно из преимуществ — возможность простого изменения метаданных для изменения интерпретации буфера массива. Изменение порядка байтов массива — это простое изменение, не требующее перестановки данных. shape массива может быть очень легко изменён без изменения буфера данных или копирования данных.
Среди прочего, что стало возможным, можно создать новый объект метаданных массива, который использует тот же буфер данных для создания нового представление того буфера данных, который имеет другое толкование буфера (например, другую форму, смещение, порядок байтов, шаги и т.д.), но использует те же байты данных. Многие операции в NumPy делают именно это, такие как slicing. Другие операции, такие как транспонирование, не перемещают элементы данных в массиве, а скорее изменяют информацию о форме и шагах, так что индексация массива меняется, но данные в массиве не перемещаются.
Обычно эти новые версии метаданных массива, но с тем же буфером данных, являются новыми представлениями буфера данных. Существует другой ndarray объект,
но использует тот же буфер данных. Вот почему необходимо принудительно создавать копии с помощью copy метод, если действительно нужно создать новую и независимую копию буфера данных.
Новые представления массивов означают увеличение счётчиков ссылок на объект для буфера данных. Простое удаление исходного объекта массива не удалит буфер данных, если другие представления его всё ещё существуют.
Проблемы порядка индексации многомерных массивов#
Смотрите также
Как правильно индексировать многомерные массивы? Прежде чем делать выводы о единственно верном способе индексирования многомерных массивов, стоит понять, почему это сложный вопрос. Этот раздел попытается подробно объяснить, как работает индексирование в NumPy и почему мы принимаем соглашение, которое используем для изображений, и когда может быть уместно принять другие соглашения.
Первое, что нужно понять — существуют два противоречащих соглашения для индексации двумерных массивов. Матричная нотация использует первый индекс для указания строки, а второй — для указания столбца. Это противоположно геометрически ориентированному соглашению для изображений, где обычно считают, что первый индекс представляет позицию x (т.е. столбец), а второй — позицию y (т.е. строку). Это само по себе является источником путаницы; пользователи, ориентированные на матрицы, и пользователи, ориентированные на изображения, ожидают разных вещей в отношении индексации.
Вторая проблема для понимания — как индексы соответствуют порядку, в котором массив хранится в памяти. В Fortran первый индекс является наиболее быстро меняющимся индексом при перемещении по элементам двумерного массива, как он хранится в памяти. Если вы примете матричное соглашение об индексации, то это означает, что матрица хранится по столбцам (поскольку первый индекс переходит к следующей строке при изменении). Таким образом, Fortran считается языком с порядком хранения по столбцам. C имеет противоположное соглашение. В C последний индекс меняется наиболее быстро при перемещении по массиву, как он хранится в памяти. Таким образом, C — язык с порядком хранения по строкам. Матрица хранится по строкам. Обратите внимание, что в обоих случаях предполагается использование матричного соглашения для индексации, т.е. для Fortran и C первый индекс — это строка. Это соглашение подразумевает, что соглашение об индексации инвариантно, а порядок данных меняется, чтобы сохранить это.
Но это не единственный способ взглянуть на это. Предположим, у нас есть большие двумерные массивы (изображения или матрицы), хранящиеся в файлах данных. Предположим, данные хранятся по строкам, а не по столбцам. Если мы хотим сохранить наше соглашение об индексации (матричное или для изображений), это означает, что в зависимости от используемого языка мы можем быть вынуждены переупорядочить данные при чтении в память, чтобы сохранить наше соглашение об индексации. Например, если мы читаем данные, упорядоченные по строкам, в память без переупорядочивания, это будет соответствовать соглашению об индексации матриц для C, но не для Fortran. И наоборот, это будет соответствовать соглашению об индексации изображений для Fortran, но не для C. Для C, если используются данные, хранящиеся в порядке строк, и нужно сохранить соглашение об индексации изображений, данные должны быть переупорядочены при чтении в память.
В конечном счёте, выбор между Fortran или C зависит от того, что важнее: не переупорядочивать данные или сохранить соглашение об индексации. Для больших изображений переупорядочивание данных потенциально затратно, и часто соглашение об индексации инвертируется, чтобы избежать этого.
Ситуация с NumPy делает эту проблему ещё более сложной. Внутренний механизм массивов NumPy достаточно гибок, чтобы принимать любой порядок индексов. Можно просто переупорядочить индексы, манипулируя внутренним шаг информацию для массивов без переупорядочивания данных вообще. NumPy будет знать, как сопоставить новый порядок индексов с данными без перемещения данных.
Итак, если это правда, почему бы не выбрать
порядок индексов, который соответствует вашим ожиданиям? В частности, почему бы не определить
изображения с порядком строк для использования соглашения об изображениях? (Это иногда называют
соглашением Fortran против соглашения C, отсюда опции порядка 'C' и 'FORTRAN'
для упорядочивания массивов в NumPy.) Недостаток этого -
потенциальные потери производительности. Обычно доступ к данным осуществляется последовательно,
либо неявно в операциях с массивами, либо явно путем перебора строк
изображения. Когда это делается, данные будут доступны в неоптимальном порядке.
При увеличении первого индекса фактически происходит последовательный доступ к элементам,
разнесенным далеко в памяти, что обычно приводит к низкой скорости доступа к памяти.
Например, для двумерного изображения im определено так, чтобы im[0, 10] представляет значение в x = 0, y = 10. Чтобы быть
последовательным с обычным поведением Python, тогда im[0] будет представлять столбец в x = 0. Однако эти данные будут распределены по всему массиву, поскольку данные
хранятся в порядке строк. Несмотря на гибкость индексирования NumPy, оно не может
действительно скрыть тот факт, что базовые операции становятся неэффективными из-за
порядка данных или что получение непрерывных подмассивов все еще неудобно (например,
im[:, 0] для первой строки, в отличие от im[0]). Таким образом, нельзя использовать идиому, такую
как for row in im; для col в im работает, но не даёт непрерывных данных столбцов.
Как оказалось, NumPy достаточно умен при работе с универсальные функции (ufuncs) чтобы определить, какой индекс является наиболее быстро изменяющимся в памяти, и использует его для самого внутреннего цикла. Таким образом, для универсальных функций (ufuncs) нет существенного преимущества ни у одного из подходов в большинстве случаев. С другой стороны, использование ndarray.flat
с массивом в порядке FORTRAN приведёт к неоптимальному доступу к памяти, так как соседние элементы в сглаженном массиве (итераторе, фактически) не являются смежными в памяти.
Действительно, факт в том, что индексирование в Python списков и других последовательностей естественно приводит к порядку от внешнего к внутреннему (первый индекс получает наибольшую группировку, следующий — меньшую, а последний — наименьший элемент). Поскольку данные изображений обычно хранятся в строках, это соответствует тому, что позиция внутри строк является последним индексируемым элементом.
Если вы действительно хотите использовать порядок Fortran, учтите, что
существует два подхода: 1) примите, что первый индекс просто не является
наиболее быстро меняющимся в памяти, и пусть все ваши процедуры ввода-вывода переупорядочивают
ваши данные при переходе из памяти на диск или наоборот, или используйте механизм NumPy
для отображения первого индекса на наиболее быстро меняющиеся данные. Мы
рекомендуем первый подход, если это возможно. Недостаток второго заключается в том, что многие
функции NumPy будут выдавать массивы без порядка Fortran, если вы не будете
осторожны при использовании order ключевое слово. Это было бы крайне неудобно.
В противном случае мы рекомендуем просто научиться обращать обычный порядок индексов при доступе к элементам массива. Конечно, это идёт вразрез с привычкой, но это больше соответствует семантике Python и естественному порядку данных.