Объяснения кода NumPy на C
Фанатизм заключается в удвоении усилий, когда вы забыли свою цель. — Джордж Сантаяна
Авторитет — это человек, который может рассказать вам больше о чем-то, чем вам действительно хочется знать. — Неизвестно
В этой главе мы пытаемся объяснить логику некоторых новых фрагментов кода. Цель этих объяснений — помочь кому-то понять идеи, лежащие в основе реализации, несколько легче, чем просто изучая код. Возможно, таким образом алгоритмы можно улучшить, позаимствовать и/или оптимизировать.
Модель памяти
Один из фундаментальных аспектов ndarray заключается в том, что массив рассматривается как «блок» памяти, начинающийся с некоторого местоположения. Интерпретация этой памяти зависит от информации о шаге. Для каждой размерности в
-мерном массиве целое число (шаг) определяет, сколько байтов нужно пропустить, чтобы перейти к следующему элементу в этой размерности. Если у вас нет массива с одним сегментом, эта информация о шаге должна учитываться при перемещении по массиву. Несложно написать код, принимающий шаги; вы просто должны использовать указатели (char *), так как шаги измеряются в байтах. Имейте в виду также, что шаги не обязательно должны быть кратными размеру элемента. Также помните, что если число измерений массива равно 0 (иногда называется массивом ранга 0), то переменные strides и dimensions равны NULL.
Помимо структурной информации, содержащейся в членах strides и dimensions объекта PyArrayObject, флаги содержат важную информацию о том, как можно получить доступ к данным. В частности, флаг NPY_ARRAY_ALIGNED устанавливается, когда память находится на подходящей границе в соответствии с типом данных массива. Даже если у вас есть непрерывный блок памяти, вы не можете просто предположить, что безопасно ссылаться на указатель к элементу, специфичному для типа данных. Только если установлен флаг NPY_ARRAY_ALIGNED, эта операция является безопасной (на некоторых платформах она будет работать, но на других, таких как Solaris, она вызовет ошибку доступа к памяти). Также необходимо убедиться, что установлен флаг NPY_ARRAY_WRITEABLE, если вы планируете записывать в область памяти массива. Также возможно получить указатель на область памяти, к которой нельзя записывать. Иногда запись в область памяти, когда флаг NPY_ARRAY_WRITEABLE не установлен, просто неприлично. В других случаях это может привести к сбою программы (например, если область данных представляет собой файл отображения памяти только для чтения).
Инкапсуляция типов данных
Тип данных — важная абстракция ndarray. Операции будут обращаться к типу данных, чтобы получить необходимую функциональность для работы с массивом. Эта функциональность предоставляется в списке указателей на функции, на которые указывает член «f» структуры PyArray_Descr. Таким образом, количество типов данных можно расширить, просто предоставив структуру PyArray_Descr с соответствующими указателями на функции в члене «f». Для встроенных типов существуют некоторые оптимизации, которые обходят эту механику, но суть абстракции типа данных заключается в возможности добавления новых типов данных.
Один из встроенных типов данных — тип данных void — позволяет использовать произвольные структурированные типы, содержащие 1 или более полей в качестве элементов массива. Поле — это просто другой объект типа данных вместе со смещением в текущем структурированном типе. Для поддержки произвольно вложенных полей реализованы несколько рекурсивных реализаций доступа к типу данных для типа void. Общей конструкцией является цикл по элементам словаря и выполнение определенной операции на основе объекта типа данных, хранящегося по данному смещению. Эти смещения могут быть произвольными числами. Поэтому необходимо учитывать возможность столкновения с невыровненными данными, если это необходимо.
N-мерные итераторы
Очень распространённая операция во многих кодах NumPy — необходимость итерирования по всем элементам общего, с шагами, N-мерного массива. Эта операция общего N-мерного цикла абстрагирована в понятии объекта итератора. Чтобы написать N-мерный цикл, вам нужно только создать объект итератора из ndarray, работать с членом dataptr структуры объекта итератора и вызвать макрос PyArray_ITER_NEXT (it) на объекте итератора, чтобы перейти к следующему элементу. «Следующий» элемент всегда находится в порядке C-contiguous.
Макрос работает, сначала делая исключения для случаев C-contiguous, 1-D и 2-D, которые работают очень просто. В общем случае итерация работает путём отслеживания списка счётчиков координат в объекте итератора. На каждой итерации последний счётчик координат увеличивается (начиная с 0). Если этот счётчик меньше, чем на единицу меньше размера массива в этой размерности (предвычисленное и сохранённое значение), счётчик увеличивается, а член dataptr увеличивается на величину шага в этой размерности, и макрос завершается. Если достигается конец размерности, счётчик последней размерности сбрасывается до нуля, а dataptr перемещается обратно в начало этой размерности, вычитая значение strides, умноженное на на единицу меньше количества элементов в этой размерности (это также предварительно вычислено и сохранено в члене backstrides объекта итератора). В этом случае макрос не завершается, но счётчик локальной размерности уменьшается, так что размерность, предшествующая последней, играет роль последней размерности, и описанные выше проверки повторно выполняются для предпоследней размерности. Таким образом, dataptr корректируется должным образом для произвольного шага.
Член coordinates структуры PyArrayIterObject сохраняет текущий N-мерный счётчик, если только базовый массив не является C-contiguous, в котором случае подсчёт координат пропускается. Член index объекта PyArrayIterObject отслеживает текущий плоский индекс итератора. Он обновляется макросом PyArray_ITER_NEXT.
Распространение
В Numeric распространение реализовывалось в нескольких строках кода, глубоко спрятанных в файле ufuncobject.c. В NumPy понятие распространения было абстрагировано, так что оно может выполняться в нескольких местах. Распространение обрабатывается функцией PyArray_Broadcast. Эта функция требует передачи объекта PyArrayMultiIterObject (или чего-то, что является двоичным эквивалентом). PyArrayMultiIterObject отслеживает число размерностей для распространения и размер в каждой размерности вместе с общим размером результата распространения. Он также отслеживает количество массивов, которые распространяются, и указатель на итератор для каждого из распространяемых массивов.
Функция PyArray_Broadcast принимает итераторы, которые уже были определены, и использует их для определения формы распространения в каждой размерности (чтобы создать итераторы одновременно с распространением, используйте функцию PyMultiIter_New). Затем итераторы корректируются таким образом, чтобы каждый итератор думал, что он итерирует по массиву с размером распространения. Это делается путём корректировки количества размерностей итераторов и формы в каждой размерности. Это работает, потому что корректируются и шаги итераторов. Распространение корректирует (или добавляет) размерности длины 1. Для этих размерностей переменная strides просто устанавливается в 0, чтобы указатель данных для итератора по этому массиву не перемещался по мере того, как операция распространения оперирует расширенной размерностью.
Распространение всегда реализовывалось в Numeric с использованием значений strides равных 0 для расширенных размерностей. Это делается точно так же в NumPy. Существенное отличие заключается в том, что теперь массив шагов отслеживается в PyArrayIterObject, итераторы, участвующие в результате распространения, отслеживаются в PyArrayMultiIterObject, и вызов PyArray_BroadCast реализует правила распространения.
Масштабируемые массивы
Масштабируемые массивы предлагают иерархию типов Python, которая позволяет установить взаимно однозначное соответствие между типом данных, хранящимся в массиве, и типом Python, который возвращается при извлечении элемента из массива. Исключение из этого правила — массивы объектов. Массивы объектов являются гетерогенными наборами произвольных объектов Python. При выборе элемента из массива объектов вы получаете исходный объект Python (а не скаляр массива объектов, который существует, но редко используется на практике).
Масштабируемые массивы также предлагают те же методы и атрибуты, что и массивы, с целью использования одного и того же кода для поддержки произвольных размерностей (включая размерности 0). Масштабируемые массивы являются только для чтения (неизменяемыми), за исключением скаляров void, к которым также можно записывать, чтобы работа с полями структурированных массивов работала более естественно (a[0]['f1'] = value).
Индексирование
Все операции индексирования Python arr[index] организуются путём предварительной подготовки индекса и поиска типа индекса. Поддерживаемые типы индексов:
- целое число
- newaxis
- срез
- ellipsis
- массивы/массивоподобные целых чисел (fancy)
- булевы (один булев массив); если в качестве индекса используется более одного булева массива или форма не совпадает точно, булев массив будет преобразован в массив целых чисел.
- 0-мерный булев (и также целое число); 0-мерные булевы массивы являются особым случаем, который должен обрабатываться в коде продвинутого индексирования. Они сигнализируют о том, что 0-мерный булев массив должен был быть интерпретирован как массив целых чисел.
А также специальный случай скалярного массива, сигнализирующий о том, что массив целых чисел был интерпретирован как целочисленный индекс, что важно, поскольку целочисленный индекс массива требует копирования, но игнорируется, если возвращается скаляр (полный целочисленный индекс). Подготовленный индекс гарантированно является допустимым, за исключением значений, выходящих за пределы границ, и ошибок трансляции для продвинутого индексирования. Это включает в себя добавление многоточия для неполных индексов, например, когда двумерный массив индексируется одним целым числом.
Следующий шаг зависит от типа найденного индекса. Если все измерения индексируются целыми числами, возвращается или устанавливается скаляр. Однозначный массив логических значений вызовет специализированные булевы функции. Индексы, содержащие многоточие или срез, но не продвинутое индексирование, всегда создают представление в старом массиве, вычисляя новые шаги и смещение памяти. Это представление затем может быть возвращено или, для присваиваний, заполнено с помощью PyArray_CopyObject. Обратите внимание, что PyArray_CopyObject также может вызываться для временных массивов в других ветвях, чтобы поддерживать сложные присваивания, когда массив имеет тип объекта.
Продвинутое индексирование
Наиболее сложным случаем является продвинутое индексирование, которое может или не может быть объединено с обычным индексированием на основе представлений. Здесь целочисленные индексы интерпретируются как индексирование на основе представлений. Прежде чем пытаться понять это, вы можете ознакомиться с его тонкостями. Код продвинутого индексирования имеет три разные ветви и один специальный случай:
- Существует один массив индексов, и он, а также массив присваивания, могут быть итерированы тривиальным образом. Например, они могут быть непрерывными. Также массив индексов должен быть типа
intpи массив значений в присваиваниях должен быть правильного типа. Это чисто быстрый путь. - Существуют только целочисленные индексы массивов, так что подмассива не существует.
- Индексирование на основе представлений и продвинутое индексирование смешаны. В этом случае индексирование на основе представлений определяет набор подмассивов, которые комбинируются продвинутым индексированием. Например,
arr[[1, 2, 3], :]создается путем вертикального объединения подмассивовarr[1, :],arr[2,:]иarr[3, :]. - Существует подмассив, но он содержит ровно один элемент. Этот случай может быть обработан так, как если бы подмассива не было, но требует некоторой осторожности при настройке.
Определение применимого случая, проверка трансляции и определение необходимого типа транспонирования выполняются в PyArray_MapIterNew. После настройки существует два случая. Если подмассива нет или он содержит только один элемент, итерация подмассива не нужна, и готовится итератор, который итерирует все массивы индексов а также массив результатов или значений. Если есть подмассив, готовятся три итератора. Один для массивов индексов, один для массива результатов или значений (без его подмассива) и один для подмассивов исходного и массива результатов/присваивания. Первые два итератора дают (или позволяют рассчитать) указатели на начало подмассива, что позволяет перезапустить итерацию подмассива.
Когда продвинутые индексы находятся рядом друг с другом, может потребоваться транспонирование. Все необходимое транспонирование обрабатывается PyArray_MapIterSwapAxes и должно обрабатываться вызывающим объектом, если только PyArray_MapIterNew не запрошено выделить результат.
После подготовки получение и установка относительно просты, хотя необходимо учитывать различные режимы итерации. Если во время получения элемента есть только один массив индексов, проверка корректности индексов выполняется предварительно. В противном случае она обрабатывается в самом внутреннем цикле для оптимизации.
Универсальные функции
Универсальные функции — это вызываемые объекты, которые принимают
входные данные и производят
выходные данные, оборачивая базовые циклы 1-D, работающие поэлементно, в полные удобные функции, которые бесшовным образом реализуют трансляцию, проверку типов и буферизованное приведение типов и обработку выходных аргументов. Новые универсальные функции обычно создаются на C, хотя существует механизм создания универсальных функций из функций Python (frompyfunc). Пользователь должен предоставить цикл 1-D, который реализует основную функцию, принимающую скалярные входные значения и помещающую полученные скаляры в соответствующие выходные слоты, как описано в реализации.
Настройка
Каждый расчет универсальной функции включает в себя некоторую издержки, связанные с настройкой расчета. Практическое значение этих издержек заключается в том, что, хотя фактический расчет универсальной функции очень быстрый, вы можете написать код, специфичный для массива и типа, который будет работать быстрее для небольших массивов, чем универсальная функция. В частности, использование универсальных функций для выполнения многих вычислений над массивами 0-D будет медленнее, чем другие решения на основе Python (молча импортированный модуль scalarmath существует именно для того, чтобы массивы скаляров имели внешний вид вычислений на основе универсальных функций с значительно меньшими накладными расходами).
При вызове универсальной функции необходимо выполнить множество действий. Информация, собранная из этих операций настройки, хранится в объекте цикла. Этот объект цикла — структура C (которая может стать объектом Python, но не инициализирована как таковая, потому что используется только во внутреннем контексте). Этот объект цикла имеет структуру, необходимую для использования с PyArray_Broadcast, чтобы трансляция обрабатывалась так же, как и в других частях кода.
Первым действием является поиск в глобальном словаре, специфичном для потока, текущих значений для размера буфера, маски ошибок и связанного объекта ошибки. Состояние маски ошибок контролирует, что происходит при обнаружении условия ошибки. Следует отметить, что проверка флагов ошибок оборудования выполняется только после выполнения каждого цикла 1-D. Это означает, что если входные и выходные массивы непрерывны и имеют правильный тип, так что выполняется один цикл 1-D, то флаги могут не проверяться до тех пор, пока не будут вычислены все элементы массива. Поиск этих значений в словаре, специфичном для потока, занимает время, которое легко игнорировать для всех, кроме очень маленьких массивов.
После проверки потоковых глобальных переменных входные данные оцениваются, чтобы определить, как должна продолжить работу универсальная функция, и входные и выходные массивы создаются при необходимости. Любые входные данные, которые не являются массивами, преобразуются в массивы (при необходимости с использованием контекста). Отмечается, какие из входных данных являются скалярами (и, следовательно, преобразуются в массивы 0-D).
Далее выбирается подходящий цикл 1-D из имеющихся у универсальной функции на основе типов входных массивов. Этот цикл 1-D выбирается путем попытки сопоставить сигнатуру типов данных входных данных с доступными сигнатурами. Сигнатуры, соответствующие встроенным типам, хранятся в члене types структуры универсальной функции. Сигнатуры, соответствующие типам, определенным пользователем, хранятся в связанном списке информации о функциях, где головной элемент хранится как CObject в словаре userloops с ключом в виде номера типа данных (используется первый тип, определенный пользователем, в списке аргументов, в качестве ключа). Сигнатуры просматриваются до тех пор, пока не будет найдена сигнатура, в которую все входные массивы могут быть безопасно преобразованы (игнорируя любые скалярные аргументы, которые не могут определить тип результата). Следствием этой процедуры поиска является то, что «меньшие типы» должны быть размещены ниже «больших типов» при хранении сигнатур. Если цикл 1-D не найден, сообщается об ошибке. В противном случае список аргументов обновляется сохраненной сигнатурой — в случае необходимости преобразования и для исправления выходных типов, предполагаемых циклом 1-D.
Если универсальная функция имеет 2 входных и 1 выходной данные и второй входной массив является массивом объектов, выполняется проверка специального случая, чтобы вернуть NotImplemented, если второй входной массив не является ndarray, не имеет атрибут __array_priority__, и не имеет специальный метод __r{op}__. Таким образом, Python получает сигнал, чтобы дать другому объекту шанс выполнить операцию вместо использования универсальных вычислений над массивами объектов. Это позволяет (например) разреженным матрицам переопределять оператор умножения циклом 1-D.
Для входных массивов, меньших заданного размера буфера, создаются копии всех несмежных, невыровненных или несовпадающих по порядку байтов массивов, чтобы гарантировать, что для малых массивов используется один цикл. Затем создаются итераторы массивов для всех входных массивов, и полученный набор итераторов транслируется в один размер.
Затем обрабатываются выходные аргументы (если они есть), и создаются любые отсутствующие выходные массивы. Если какой-либо предоставленный выходной массив не имеет правильного типа (или невыровнен) и меньше размера буфера, то создается новый выходной массив со флагом UPDATEIFCOPY, чтобы при DECREF после завершения функции его содержимое было скопировано обратно в выходной массив. Затем обрабатываются итераторы для выходных аргументов.
Наконец, принимается решение о том, как выполнить механизм цикла, чтобы убедиться, что все элементы входных массивов комбинируются для получения выходных массивов правильного типа. Варианты выполнения цикла: один цикл (для непрерывных, выровненных и правильного типа данных), цикл с шагом (для несмежных, но все еще выровненных и правильного типа данных) и буферизованный цикл (для невыровненных или неправильного типа данных). В зависимости от вызываемого метода выполнения, цикл затем настраивается и вычисляется.
Вызов функции
В этом разделе описывается, как настраивается и выполняется основной цикл вычисления универсальной функции для каждого из трех различных типов выполнения. Если NPY_ALLOW_THREADS определено во время компиляции, то, до тех пор пока не будут задействованы массивы объектов, блокировка глобального интерпретатора Python (GIL) освобождается перед вызовом циклов. При необходимости она восстанавливается для обработки условий ошибок. Флаги ошибок оборудования проверяются только после завершения цикла 1-D.
Один цикл
Это самый простой случай. Универсальная функция выполняется путем вызова базового цикла 1-D ровно один раз. Это возможно только тогда, когда у нас есть выровненные данные правильного типа (включая порядок байтов) для входных и выходных данных, и все массивы имеют одинаковые шаги (или непрерывные, 0-D или 1-D). В этом случае цикл вычислений 1-D вызывается один раз для вычисления расчета для всего массива. Обратите внимание, что флаги ошибок оборудования проверяются только после завершения всего вычисления.
Цикл с шагом
Когда входные и выходные массивы выровнены и имеют правильный тип, но шаги не одинаковы (несмежные и 2-D или более), для расчета используется вторая структура цикла. Этот подход преобразует все итераторы входных и выходных аргументов в итерацию по всем, кроме самого большого измерения. Затем внутренний цикл обрабатывается базовым циклом вычислений 1-D. Внешний цикл — это стандартный цикл итерации по преобразованным итераторам. Флаги ошибок оборудования проверяются после завершения каждого цикла 1-D.
Буферизованный цикл
Это код, обрабатывающий ситуации, когда входные и/или выходные массивы не выровнены или имеют неправильный тип данных (включая перестановку байтов) по сравнению с ожидаемым подлежащим 1-мерным циклом. Также предполагается, что массивы не являются непрерывными. Код очень похож на код с шагом-циклом, за исключением того, что внутренний 1-мерный цикл изменен так, что предварительная обработка выполняется для входных данных, а пост-обработка — для выходных данных в блоках bufsize (где bufsize — параметр, настраиваемый пользователем). Подлежащий 1-мерный вычислительный цикл вызывается с данными, которые копируются (если это необходимо). Код настройки и код цикла в этом случае значительно сложнее, потому что ему нужно обрабатывать:
- выделение памяти для временных буферов
- определение, использовать ли буферы для входных и выходных данных (невыровненные и/или неправильный тип данных)
- копирование и возможно преобразование данных для любых входных или выходных данных, для которых необходимы буферы.
- обработку массивов типа Object, чтобы корректно обрабатывать счетчики ссылок при необходимости копирования и/или преобразования.
- разбиение внутреннего 1-мерного цикла на блоки bufsize (с возможным остатком).
Опять же, флаги ошибок оборудования проверяются в конце каждого 1-мерного цикла.
Конечная обработка вывода
Ufuncs позволяют беспрепятственно передавать другие массивоподобные классы через интерфейс, так как входы определенного класса будут вызывать выходы того же самого класса. Механизм, посредством которого это работает, следующий. Если какой-либо из входов не является ndarrays и определяет метод __array_wrap__, то класс с наибольшим атрибутом __array_priority__ определяет тип всех выходов (за исключением любых выходных массивов, переданных в качестве аргументов). Метод __array_wrap__ входного массива будет вызван с ndarray, возвращаемым ufunc, в качестве входного значения. Поддерживаются два стиля вызова функции __array_wrap__. Первый принимает ndarray в качестве первого аргумента и кортеж «контекста» во втором. Контекст — это (ufunc, аргументы, номер выходного аргумента). Это первый вызов, который будет предпринят. Если произойдет ошибка TypeError, то функция вызывается только с ndarray в качестве первого аргумента.
Методы
Существует три метода ufuncs, которые требуют расчета, аналогичного общим методам ufuncs. Это reduce, accumulate и reduceat. Каждый из этих методов требует команды настройки, за которой следует цикл. Возможны четыре стиля циклов для этих методов: без элементов, с одним элементом, с шагом-циклом и с буферизованным циклом. Это те же основные стили циклов, что и для вызова общей функции, за исключением случаев без элементов и с одним элементом, которые являются особыми случаями, возникающими, когда входные массивы-объекты содержат 0 и 1 элемент соответственно.
Настройка
Функция настройки для всех трех методов — construct_reduce. Эта функция создает объект цикла уменьшения и заполняет его параметрами, необходимыми для завершения цикла. Все методы работают только с ufuncs, принимающими 2 входа и возвращающими 1 выход. Поэтому подлежащий 1-мерный цикл выбирается, предполагая сигнатуру [ otype, otype, otype ], где otype — запрашиваемый тип данных уменьшения. Размер буфера и обработка ошибок извлекаются из глобального хранилища (по потоку). Для небольших массивов, которые не выровнены или имеют неправильный тип данных, создается копия, чтобы использовать небуферизованную часть кода. Затем выбирается стратегия циклирования. Если в массиве 1 элемент или 0 элементов, выбирается простой метод циклирования. Если массив выровнен и имеет правильный тип данных, выбирается шаговый цикл. В противном случае должен быть выполнен буферизованный цикл. Затем устанавливаются параметры циклирования, и создается возвращаемый массив. Форма выходного массива отличается в зависимости от того, является ли метод reduce, accumulate или reduceat. Если выходной массив уже предоставлен, проверяется его форма. Если выходной массив не является C-непрерывным, не выровненным и не имеет правильного типа данных, создается временная копия с установленным флагом UPDATEIFCOPY. Таким образом, методы смогут работать с хорошо себя ведущим выходным массивом, но результат будет скопирован обратно в истинный выходной массив после завершения вычислений метода. Наконец, настраиваются итераторы для циклирования по правильной оси (в зависимости от значения axis, переданного методу), и процедура настройки возвращается к фактической процедуре вычисления.
Reduce
Все методы ufunc используют одни и те же подлежащие 1-мерные вычислительные циклы с входными и выходными аргументами, скорректированными для выполнения соответствующего уменьшения. Например, ключ к работе reduce заключается в том, что 1-мерный цикл вызывается с выходом и вторым входом, указывающим на ту же позицию в памяти, и оба имеют шаг-размер 0. Первый вход указывает на входной массив со значением шага, заданным соответствующим шагом для выбранной оси. Таким образом, выполняемая операция
![\begin{align*}
o & = & i[0] \\
o & = & i[k]\textrm{<op>}o\quad k=1\ldots N
\end{align*}](https://docs.scipy.org/doc/numpy-1.10.1/_images/math/e84ac41da48293683e7736944c7b697b131a4dca.png)
где
— количество элементов в входе,
,
— выход, а
—
элемент
по выбранной оси. Эта основная операция повторяется для массивов с более чем 1 измерением, чтобы уменьшение происходило для каждого 1-мерного подмассива по выбранной оси. Итератор с удаленным выбранным измерением обрабатывает это циклирование.
Для буферизованных циклов необходимо позаботиться о копировании и преобразовании данных перед вызовом функции цикла, поскольку подлежащий цикл ожидает выровненные данные правильного типа данных (включая порядок байтов). Буферизованный цикл должен обрабатывать это копирование и преобразование перед вызовом функции цикла для блоков, не превышающих заданный пользователем размер bufsize.
Accumulate
Функция accumulate очень похожа на функцию reduce в том, что выход и второй вход оба указывают на выход. Разница заключается в том, что второй вход указывает на память на один шаг позади текущего указателя вывода. Таким образом, выполняемая операция
![\begin{align*}
o[0] & = & i[0] \\
o[k] & = & i[k]\textrm{<op>}o[k-1]\quad k=1\ldots N.
\end{align*}](https://docs.scipy.org/doc/numpy-1.10.1/_images/math/e1486dfed0186fae30752b33ae67b1cf7c705419.png)
Выход имеет такую же форму, как вход, и каждый 1-мерный цикл работает над
элементами, когда форма по выбранной оси равна
. Опять же, буферизованные циклы позаботятся о копировании и преобразовании данных перед вызовом подлежащего 1-мерного вычислительного цикла.
Reduceat
Функция reduceat является обобщением функций reduce и accumulate. Она реализует уменьшение по диапазонам входного массива, указанных индексами. Дополнительный аргумент indices проверяется, чтобы убедиться, что каждый вход не слишком велик для входного массива по выбранному измерению перед выполнением вычислений цикла. Реализация цикла обрабатывается кодом, очень похожим на код reduce, повторяемым столько раз, сколько элементов в входных данных indices. В частности: указатель первого входа, переданный подлежащему 1-мерному вычислительному циклу, указывает на входной массив в правильном месте, указанном массивом индексов. Кроме того, указатель вывода и указатель второго входа, переданные подлежащему 1-мерному циклу, указывают на одну и ту же позицию в памяти. Размер 1-мерного вычислительного цикла фиксируется как разность между текущим индексом и следующим индексом (если текущий индекс является последним индексом, то следующий индекс предполагается равным длине массива по выбранному измерению). Таким образом, 1-мерный цикл будет реализовывать уменьшение по указанным индексам.
Невыровненные или тип данных цикла, не соответствующие типу данных входных и/или выходных данных, обрабатываются с помощью буферизованного кода, в котором данные копируются во временный буфер и преобразуются в правильный тип данных, если это необходимо, перед вызовом подлежащей 1-мерной функции. Временные буферы создаются размером не более заданного пользователем значения размера буфера. Таким образом, цикл должен быть достаточно гибким, чтобы вызывать подлежащий 1-мерный вычислительный цикл достаточно много раз для завершения всего расчета частями размером не более размера буфера.
© 2008–2016 NumPy Developers
Licensed under the NumPy License.
https://docs.scipy.org/doc/numpy-1.10.1/reference/internals.code-explanations.html