Объяснения кода 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-непрерывном порядке. Макрос работает, сначала выполняя специализированные случаи для C-непрерывных, 1-D и 2-D случаев, которые работают очень просто.
В общем случае итерация работает путем отслеживания списка счётчиков координат в объекте итератора. На каждой итерации последний счётчик координат увеличивается (начиная с 0). Если этот счётчик меньше, чем значение на один меньше размера массива в данной размерности (предвычисленное и сохранённое значение), то счётчик увеличивается, и член dataptr увеличивается на шаги в данной размерности, и макрос заканчивается. Если конец размерности достигнут, счётчик последней размерности сбрасывается в ноль, и dataptr перемещается обратно в начало этой размерности, вычитая значение шага, умноженное на значение на один меньше, чем количество элементов в этой размерности (это также предварительно вычислено и сохранено в члене backstrides объекта итератора). В этом случае макрос не заканчивается, а локальный счётчик размерности уменьшается, так что размерность, следующая за последней, берёт на себя роль последней размерности, и описанные выше тесты снова выполняются для предпоследней размерности. Таким образом, dataptr корректируется должным образом для произвольных шагов.
Член coordinates структуры PyArrayIterObject сохраняет текущий N-мерный счётчик, если только базовый массив не является C-непрерывным, в этом случае подсчёт координат пропускается. Член index структуры PyArrayIterObject отслеживает текущий плоский индекс итератора. Он обновляется макросом PyArray_ITER_NEXT.
Вещание
В Numeric, предшественнике NumPy, вещание реализовывалось в нескольких строках кода, глубоко закопанных в файле ufuncobject.c. В NumPy понятие вещания было абстрагировано, чтобы его можно было использовать в нескольких местах. Вещание обрабатывается функцией PyArray_Broadcast. Эта функция требует передачи объекта PyArrayMultiIterObject (или его бинарного эквивалента). Объект PyArrayMultiIterObject отслеживает количество измерений вещания и размер в каждой размерности, а также общий размер результата вещания. Он также отслеживает количество вещаемых массивов и указатель на итератор для каждого из вещаемых массивов.
Функция PyArray_Broadcast принимает уже определённые итераторы и использует их для определения формы вещания в каждой размерности (чтобы создать итераторы одновременно с вещанием, используйте функцию PyArray_MultiIterNew). Затем итераторы корректируются так, чтобы каждый итератор думал, что он итерирует по массиву с размером вещания. Это делается путём корректировки числа измерений и формы итераторов в каждой размерности. Это работает, потому что шаги итератора также корректируются. Вещание корректирует (или добавляет) размерности длиной 1. Для этих размерностей переменная strides просто устанавливается в 0, так что указатель данных для итератора по этому массиву не перемещается по мере того, как операция вещания работает по расширенной размерности.
Вещание всегда реализовывалось в Numeric с помощью шагов со значением 0 для расширенных размерностей. В NumPy это делается точно так же. Существенная разница заключается в том, что теперь массив шагов отслеживается в объекте PyArrayIterObject, итераторы, участвующие в результате вещания, отслеживаются в объекте PyArrayMultiIterObject, а вызов PyArray_Broadcast реализует правила вещания.
Массивовые скаляры
Массивовые скаляры предлагают иерархию типов Python, которые позволяют установить взаимно однозначное соответствие между типом данных, хранящимся в массиве, и типом Python, который возвращается при извлечении элемента из массива. Исключение из этого правила было сделано для массивов объектов. Массивы объектов — это гетерогенные коллекции произвольных объектов Python. Когда вы выбираете элемент из массива объектов, вы получаете исходный объект Python (а не скаляр массива объектов, который существует, но редко используется в практических целях).
Массивовые скаляры также предлагают те же методы и атрибуты, что и массивы, с целью использования одного и того же кода для поддержки произвольных измерений (включая 0 измерений). Массивовые скаляры являются неизменяемыми (только для чтения), за исключением скаляра void, к которому также можно записывать, чтобы установка поля структурированного массива работала более естественно (a[0][‘f1’] = value ).
Индексирование
Все операции индексирования Python arr[index] организованы путём предварительной подготовки индекса и нахождения типа индекса. Поддерживаемые типы индексов:
- целое число
- newaxis
- срез
- многоточие
- массивы целых чисел/массивоподобные (fancy)
- булевы (один массив булевых значений); если индексом выступает более одного массива булевых значений или форма не совпадает точно, массив булевых значений будет преобразован в массив целых чисел.
- 0-мерный булевый (а также целочисленный); 0-мерные булевы массивы — это особый случай, который необходимо обрабатывать в коде продвинутого индексирования. Они сигнализируют о том, что 0-мерный булев массив должен был быть интерпретирован как массив целых чисел.
А также особый случай скалярного массива, сигнализирующий о том, что массив целых чисел был интерпретирован как целочисленный индекс, что важно, потому что целочисленный индекс массива заставляет копировать данные, но игнорируется, если возвращается скаляр (полный целочисленный индекс). Подготовленный индекс гарантированно будет валидным, за исключением значений, выходящих за пределы границ, и ошибок трансляции для продвинутого индексирования. Это включает добавление эллипса для неполных индексов, например, когда двумерный массив индексируется одним целым числом.
Следующий шаг зависит от типа найденного индекса. Если все измерения индексируются целыми числами, возвращается или устанавливается скаляр. Единственный массив булевых индексов вызовет специализированные булевы функции. Индексы, содержащие эллипс или срез, но без продвинутого индексирования, всегда создадут представление в старом массиве, рассчитав новые шаги и смещение памяти. Это представление может быть возвращено или, для присваиваний, заполнено с помощью PyArray_CopyObject. Обратите внимание, что PyArray_CopyObject также может быть вызвано для временных массивов в других ветвях, чтобы поддержать сложные присваивания, когда массив имеет тип object.
Продвинутое индексирование
Наиболее сложным случаем является продвинутое индексирование, которое может быть или не быть комбинировано с типичным индексированием на основе представлений. Здесь целочисленные индексы интерпретируются как индексирование на основе представлений. Прежде чем пытаться понять это, вы можете ознакомиться с его тонкостями. Код продвинутого индексирования имеет три различные ветви и один особый случай:
- Существует один массив индексов, и он, а также массив присваивания, могут быть тривиально итерируемы. Например, они могут быть смежными. Также массив индексов должен быть типа
intp, а массив значений при присваивании должен иметь правильный тип. Это чисто быстрый путь. - Существуют только целочисленные индексы массивов, так что никакого подмассива не существует.
- Индексирование на основе представлений и продвинутое индексирование смешаны. В этом случае индексирование на основе представлений определяет набор подмассивов, которые объединяются продвинутым индексированием. Например,
arr[[1, 2, 3], :]создается путем вертикального объединения подмассивовarr[1, :],arr[2,:]иarr[3, :]. - Существует подмассив, но он содержит ровно один элемент. Этот случай может быть обработан так, как будто подмассива нет, но требует некоторой осторожности во время настройки.
Решение, какой случай применим, проверка трансляции и определение необходимого типа транспонирования выполняются в PyArray_MapIterNew. После настройки существуют два случая. Если нет подмассива или он содержит только один элемент, итерация по подмассивам не требуется, и подготавливается итератор, который итерирует все массивы индексов а также массив результата или значение. Если есть подмассив, подготавливаются три итератора. Один для массивов индексов, один для массива результата или значения (за вычетом его подмассива) и один для подмассивов исходного и массива результата/присваивания. Первые два итератора предоставляют (или позволяют рассчитать) указатели в начало подмассива, что затем позволяет перезапустить итерацию по подмассиву.
Когда продвинутые индексы находятся рядом друг с другом, может потребоваться транспонирование. Все необходимые транспонирования обрабатываются PyArray_MapIterSwapAxes и должны обрабатываться вызывающим объектом, если не запрошено задание PyArray_MapIterNew результатов.
После подготовки получение и установка относительно просты, хотя необходимо учитывать различные режимы итерации. Если во время получения элемента существует только один массив индексов, то проверка валидности индексов выполняется предварительно. В противном случае она обрабатывается в самом внутреннем цикле для оптимизации.
Универсальные функции
Универсальные функции — это вызываемые объекты, которые принимают входные данные и производят
выходные данные, обертывая базовые циклы по одному измерению, которые работают с элементом за элементом в полноценные простые в использовании функции, которые бесшовно реализуют трансляцию, проверку типов и буферизованное приведение, а также обработку выходных аргументов. Новые универсальные функции обычно создаются на C, хотя существует механизм создания универсальных функций из функций Python (
frompyfunc). Пользователь должен предоставить цикл по одному измерению, который реализует основную функцию, принимающую входные скалярные значения и помещающую полученные скаляры в соответствующие выходные слоты, как описано в реализации.
Настройка
Каждый расчет универсальной функции включает некоторые накладные расходы, связанные с настройкой вычисления. Практическое значение этих накладных расходов заключается в том, что, хотя фактический расчет универсальной функции очень быстрый, вы сможете написать код, специфичный для массивов и типов, который будет работать быстрее для малых массивов, чем универсальная функция. В частности, использование универсальных функций для выполнения многих вычислений над 0-мерными массивами будет медленнее, чем другие решения на основе Python (внутри молча импортированный модуль scalarmath существует именно для того, чтобы массивам-скалярам придать вид и ощущения вычислений на основе универсальных функций с существенно уменьшенными накладными расходами).
При вызове универсальной функции необходимо выполнить множество действий. Информация, собранная в ходе этих операций настройки, хранится в объекте цикла. Этот объект цикла — структура C (которая могла бы стать объектом Python, но не инициализируется как таковой, поскольку используется только во внутренней части). Этот объект цикла имеет структуру, необходимую для использования с PyArray_Broadcast, чтобы трансляция обрабатывалась так же, как и в других разделах кода.
Первым шагом является поиск в потокоспецифическом глобальном словаре текущих значений размера буфера, маски ошибок и связанного объекта ошибок. Состояние маски ошибок контролирует, что происходит, когда обнаружено условие ошибки. Следует отметить, что проверка флагов аппаратных ошибок выполняется только после выполнения каждого цикла по одному измерению. Это означает, что если входные и выходные массивы смежные и имеют правильный тип, так что выполняется один цикл по одному измерению, то флаги могут не проверяться до тех пор, пока не будут вычислены все элементы массива. Поиск этих значений в потокоспецифическом словаре занимает время, которое легко игнорировать для всех, кроме очень малых массивов.
После проверки потокоспецифических глобальных переменных входные данные оцениваются, чтобы определить, как должна продолжить работу универсальная функция, и входные и выходные массивы создаются при необходимости. Любые входные данные, которые не являются массивами, преобразуются в массивы (с использованием контекста при необходимости). Отмечается, какие из входных данных являются скалярами (и поэтому преобразуются в 0-мерные массивы).
Далее выбирается подходящий цикл по одному измерению из доступных универсальной функции на основе типов входных массивов. Этот цикл по одному измерению выбирается путем попытки сопоставить сигнатуру типов данных входных данных с доступными сигнатурами. Сигнатуры, соответствующие встроенным типам, хранятся в члене types структуры универсальной функции. Сигнатуры, соответствующие типам, определенным пользователем, хранятся в связанном списке информации о функциях, где головной элемент хранится как CObject в словаре userloops, индексированном по номеру типа данных (первый тип, определенный пользователем, в списке аргументов, используется в качестве ключа). Сигнатуры ищутся до тех пор, пока не будет найдена сигнатура, в которую все входные массивы могут быть безопасно преобразованы (игнорируя любые скалярные аргументы, которые не могут определить тип результата). Следствием этой процедуры поиска является то, что «более мелкие типы» должны располагаться ниже «более крупных типов», когда сигнатуры хранятся. Если цикл по одному измерению не найден, то сообщается об ошибке. В противном случае список аргументов обновляется сохраненной сигнатурой — в случае необходимости преобразования и для исправления типов выходных данных, предполагаемых циклом по одному измерению.
Если универсальная функция имеет 2 входных и 1 выходной аргумент, а второй входной аргумент — массив объекта, выполняется специальная проверка, чтобы вернуть NotImplemented, если второй входной аргумент не является ndarray, имеет атрибут __array_priority__ и имеет специальный метод __r{op}__. Таким образом, Python сигнализируется, чтобы дать другому объекту шанс выполнить операцию, а не использовать общие вычисления массивов объектов. Это позволяет (например), разреженным матрицам переопределять оператор умножения циклом по одному измерению.
Для входных массивов, меньших указанного размера буфера, выполняются копии всех несмежных, невыровненных или несоответствующих байтовому порядку массивов, чтобы гарантировать, что для малых массивов используется один цикл. Затем создаются итераторы массивов для всех входных массивов, и полученная коллекция итераторов транслируется в одну форму.
Затем обрабатываются выходные аргументы (если есть), и создаются любые отсутствующие выходные массивы. Если какой-либо предоставленный выходной массив не имеет правильного типа (или невыровнен) и меньше размера буфера, то новый выходной массив создается со специальным флагом NPY_ARRAY_WRITEBACKIFCOPY. В конце функции вызывается PyArray_ResolveWritebackIfCopy, чтобы его содержимое было скопировано обратно в выходной массив. Затем обрабатываются итераторы для выходных аргументов.
Наконец, принимается решение о том, как выполнить механизм цикла, чтобы гарантировать, что все элементы входных массивов комбинируются для создания выходных массивов правильного типа. Варианты выполнения цикла — один цикл (для смежных, выровненных и правильного типа данных), цикл с шагами (для несмежных, но все еще выровненных и правильного типа данных) и буферизованный цикл (для невыровненных или некорректного типа данных). В зависимости от вызываемого метода выполнения цикл настраивается и вычисляется.
Вызов функции
В этом разделе описано, как настраивается и выполняется базовый цикл вычислений универсальной функции для каждого из трех различных типов выполнения. Если NPY_ALLOW_THREADS определен во время компиляции, то до вызова циклов, пока не задействованы массивы объектов, отпускается блокировка интерпретатора Python (GIL). Она повторно приобретается при необходимости для обработки условий ошибок. Флаги аппаратных ошибок проверяются только после завершения цикла по одному измерению.
Один цикл
Это самый простой случай. Универсальная функция выполняется путем вызова базового цикла по одному измерению ровно один раз. Это возможно только тогда, когда у нас есть выровненные данные правильного типа (включая байтовый порядок) для входных и выходных данных, и все массивы имеют одинаковые шаги (смежные, 0-мерные или 1-мерные). В этом случае цикл вычислений по одному измерению вызывается один раз для вычисления расчета для всего массива. Обратите внимание, что флаги аппаратных ошибок проверяются только после завершения всего расчета.
Цикл с шагами
Когда входные и выходные массивы выровнены и имеют правильный тип, но шаги не однородны (несмежные и 2-мерные или больше), для расчета используется вторая структура цикла. Этот подход преобразует все итераторы для входных и выходных аргументов в итераторы по всем измерениям, кроме самого большого. Внутренний цикл затем обрабатывается базовым циклом вычислений по одному измерению. Внешний цикл — это стандартный цикл итерации по преобразованным итераторам. Флаги аппаратных ошибок проверяются после завершения каждого цикла по одному измерению.
Буферизованный цикл
Данный код обрабатывает ситуации, когда входные и/или выходные массивы не выровнены или имеют неправильный тип данных (включая переставленные байты), по сравнению с тем, что ожидает внутренний цикл по одному измерению. Также предполагается, что массивы не являются непрерывными. Код очень похож на код цикла с шагом, за исключением того, что внутренний цикл по одному измерению модифицирован таким образом, что предварительная обработка выполняется над входными данными, а последующая обработка — над выходными данными в блоках по bufsize элементов (где bufsize — параметр, задаваемый пользователем). Внутренний вычислительный цикл по одному измерению вызывается для скопированных данных (если это необходимо). Код настройки и код цикла в этом случае значительно сложнее, поскольку он должен обрабатывать:
- выделение памяти для временных буферов
- определение, следует ли использовать буферы для входных и выходных данных (невыровненные данные и/или неправильный тип данных)
- копирование и, возможно, приведение типов данных для всех входных или выходных данных, для которых необходимы буферы.
- специальную обработку массивов типа Object, чтобы правильно обрабатывать счётчики ссылок при копировании и/или приведении типов.
- разбиение внутреннего цикла по одному измерению на блоки по bufsize элементов (с возможным остатком).
Опять же, флаги ошибок оборудования проверяются в конце каждого цикла по одному измерению.
Конечная обработка выходных данных
Ufuncs позволяют без проблем передавать другие массивоподобные классы через интерфейс в том смысле, что входные данные определённого класса приведут к тому, что выходные данные будут иметь тот же класс. Механизм работы следующий. Если любой из входных данных не является ndarray и определяет метод __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 выход. Следовательно, внутренний цикл по одному измерению выбирается при предположении подписи [ otype, otype, otype ], где otype — запрашиваемый тип данных для сокращения. Размер буфера и обработка ошибок затем извлекаются из глобального хранилища (по потоку). Для небольших массивов, которые не выровнены или имеют неправильный тип данных, создаётся копия, чтобы использовать небуферизованный фрагмент кода. Затем выбирается стратегия циклирования. Если в массиве 1 элемент или 0 элементов, то выбирается простой метод циклирования. Если массив не невыровнен и имеет правильный тип данных, то выбирается циклирование с шагом. В противном случае необходимо выполнить буферизованное циклирование. Параметры циклирования устанавливаются, и создаётся возвращаемый массив. Форма выходного массива отличается в зависимости от того, является ли метод reduce, accumulate или reduceat. Если выходной массив уже задан, то проверяется его форма. Если выходной массив не является C-непрерывным, невыровненным и не имеет правильного типа данных, то создаётся временная копия с установленным флагом WRITEBACKIFCOPY. Таким образом, методы смогут работать с хорошо себя ведущим выходным массивом, но результат будет скопирован обратно в фактический выходной массив, когда вызывается PyArray_ResolveWritebackIfCopy по завершении функции. Наконец, создаются итераторы для циклирования по нужному осям (в зависимости от значения оси, переданного методу), и процедура настройки возвращается к фактической вычислительной процедуре.
Reduce
Все методы ufunc используют те же самые внутренние циклы вычислений по одному измерению с входными и выходными аргументами, скорректированными таким образом, чтобы выполнялось соответствующее сокращение. Например, ключевым моментом работы reduce является то, что цикл по одному измерению вызывается с выходом и вторым входом, указывающими на одно и то же положение в памяти, и оба имеют шаг 0. Первый вход указывает на входной массив со значением шага, задаваемого соответствующим шагом для выбранной оси. Таким образом, выполняемая операция —
где — количество элементов во входном массиве,
,
— выход, а
—
элемент
вдоль выбранной оси. Эта базовая операция повторяется для массивов с большей, чем 1, размерностью, так что сокращение выполняется для каждого одномерного подмассива вдоль выбранной оси. Итератор, у которого удалена выбранная размерность, обрабатывает это циклирование.
Для буферизованных циклов необходимо позаботиться о копировании и приведении типов данных перед вызовом функции цикла, потому что внутренний цикл ожидает выровненные данные правильного типа данных (включая порядок байтов). Буферизованный цикл должен выполнить это копирование и приведение типов перед вызовом функции цикла для блоков, не превышающих заданный пользователем bufsize.
Accumulate
Функция accumulate очень похожа на функцию reduce в том, что выход и второй вход оба указывают на выход. Разница в том, что второй вход указывает на память на один шаг позади текущего указателя выхода. Таким образом, выполняемая операция —
Выход имеет ту же форму, что и вход, и каждый цикл по одному измерению выполняется над элементами, когда форма по выбранной оси равна
. Опять же, буферизованные циклы позаботятся о копировании и приведении типов данных перед вызовом внутреннего вычислительного цикла по одному измерению.
Reduceat
Функция reduceat является обобщением функций reduce и accumulate. Она реализует сокращение по диапазонам входного массива, указанным индексами. Дополнительный аргумент индексов проверяется, чтобы убедиться, что каждый вход не слишком велик для входного массива вдоль выбранного измерения, прежде чем будут выполнены вычисления цикла. Реализация цикла обрабатывается кодом, очень похожим на код reduce, повторяемый столько раз, сколько элементов в входном индексе. В частности: указатель первого входа, переданный внутреннему вычислительному циклу по одному измерению, указывает на входной массив в правильном месте, указанном массивом индексов. Кроме того, указатель выхода и указатель второго входа, переданные внутреннему циклу по одному измерению, указывают на одно и то же место в памяти. Размер цикла вычислений по одному измерению фиксируется как разность между текущим индексом и следующим индексом (когда текущий индекс — последний индекс, то следующий индекс предполагается равным длине массива вдоль выбранного измерения). Таким образом, цикл по одному измерению будет реализовывать сокращение по указанным индексам.
Обработка невыровненных данных или типов данных цикла, не соответствующих типу данных входных и/или выходных данных, выполняется с помощью буферизованного кода, в котором данные копируются во временной буфер и приводятся к нужному типу данных, если это необходимо, перед вызовом основной функции одного измерения. Временные буферы создаются размером (элементов) не больше заданного пользователем значения размера буфера. Таким образом, цикл должен быть достаточно гибким, чтобы вызывать основной вычислительный цикл по одному измерению достаточно раз, чтобы завершить все вычисления частями, не превышающими размер буфера.
© 2005–2021 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.20/reference/internals.code-explanations.html