За рамками основ
Итерирование по элементам массива
Базовое итерирование
Одной из распространенных алгоритмических задач является обход всех элементов многомерного массива. Объект итератора массива делает это легко и обобщенно для массивов любой размерности. Естественно, если вам известна размерность массива, вы всегда можете использовать вложенные циклы for для выполнения итерирования. Однако, если вы хотите написать код, который работает с любым числом измерений, вы можете использовать итератор массива. Объект итератора массива возвращается при обращении к атрибуту .flat массива.
Базовое использование заключается в вызове PyArray_IterNew ( array ), где массив является объектом ndarray (или одним из его подклассов). Возвращаемый объект — это объект итератора массива (тот же объект, что и возвращает атрибут .flat объекта ndarray). Этот объект обычно приводится к типу PyArrayIterObject*, чтобы получить доступ к его членам. Необходимы только следующие члены: iter->size , который содержит общий размер массива, iter->index, который содержит текущий одномерный индекс в массиве, и iter->dataptr, который является указателем на данные текущего элемента массива. Иногда также полезно получить доступ к iter->ao, который является указателем на базовый объект ndarray.
После обработки данных в текущем элементе массива, следующий элемент массива можно получить, используя макрос PyArray_ITER_NEXT ( iter ). Итерирование всегда происходит в непрерывном порядке (индекс последнего элемента изменяется быстрее всего). PyArray_ITER_GOTO ( iter, destination ) можно использовать для перехода к определенной точке в массиве, где destination — массив типа npy_intp с пространством, достаточным для обработки по крайней мере количества измерений в базовом массиве. Иногда полезно использовать PyArray_ITER_GOTO1D ( iter, index ), который перейдет к одномерному индексу, заданному значением index. Однако наиболее распространенное использование показано в следующем примере.
PyObject *obj; /* assumed to be some ndarray object */
PyArrayIterObject *iter;
...
iter = (PyArrayIterObject *)PyArray_IterNew(obj);
if (iter == NULL) goto fail; /* Assume fail has clean-up code */
while (iter->index < iter->size) {
/* do something with the data at it->dataptr */
PyArray_ITER_NEXT(it);
}
...
Вы также можете использовать PyArrayIter_Check ( obj ), чтобы убедиться, что у вас есть объект итератора, и PyArray_ITER_RESET ( iter ), чтобы сбросить объект итератора в начало массива.
Следует подчеркнуть, что итератор массива может быть не нужен, если ваш массив уже непрерывен (использование итератора массива будет работать, но медленнее, чем самый быстрый код, который вы могли бы написать). Основное назначение итераторов массива — обобщенное итерирование по N-мерным массивам с произвольными шагами. Они используются во многих частях самого исходного кода NumPy. Если вы уже знаете, что ваш массив непрерывен (Fortran или C), просто добавляя размер элемента к текущему значению указателя, вы очень эффективно пройдете по массиву. Другими словами, в случае непрерывности такой код, вероятно, будет быстрее (при условии, что используются double).
npy_intp size;
double *dptr; /* could make this any variable type */
size = PyArray_SIZE(obj);
dptr = PyArray_DATA(obj);
while(size--) {
/* do something with the data at dptr */
dptr++;
}
Итерирование по всем осям, кроме одной
Распространенный алгоритм — цикл по всем элементам массива и выполнение некоторой функции с каждым элементом путем вызова функции. Поскольку вызовы функций могут быть ресурсоемкими, один из способов ускорить такой алгоритм — написать функцию, которая принимает вектор данных, а затем написать итерацию, чтобы вызов функции выполнялся для всего измерения данных за один раз. Это увеличивает объем работы, выполняемой за один вызов функции, тем самым уменьшая накладные расходы вызова функции до небольшой(ей) доли от общего времени. Даже если внутренняя часть цикла выполняется без вызова функции, может быть выгодно выполнить внутренний цикл по измерению с наибольшим количеством элементов, чтобы воспользоваться преимуществами ускорения, доступными на микропроцессорах, которые используют конвейерную обработку для повышения эффективности основных операций.
Функция PyArray_IterAllButAxis ( array, &dim ) создает объект итератора, который модифицируется так, что он не будет итерировать по измерению, указанному dim. Единственное ограничение для этого объекта итератора заключается в том, что макрос PyArray_ITER_GOTO1D ( it, ind ) использовать нельзя (следовательно, индексация в виде плоского массива не будет работать, если вы передадите этот объект в Python — поэтому этого делать не следует). Обратите внимание, что возвращаемый этим вызовом объект по-прежнему обычно приводится к типу PyArrayIterObject*. Всё, что было сделано, — это модификация шагов и измерений возвращенного итератора для моделирования итерирования по массиву array[…,0,…] где 0 размещен в измерении. Если dim отрицательный, то определяется измерение с наибольшей осью и используется оно.
Итерирование по нескольким массивам
Очень часто желательно итерировать по нескольким массивам одновременно. Универсальные функции — пример такого поведения. Если вам нужно только итерировать по массивам с одинаковой формой, то стандартная процедура — просто создание нескольких объектов итераторов. Например, следующий код итерирует по двум массивам, предполагая, что они имеют одинаковую форму и размер (фактически, obj1 должен иметь по крайней мере столько же элементов, сколько и obj2):
/* It is already assumed that obj1 and obj2
are ndarrays of the same shape and size.
*/
iter1 = (PyArrayIterObject *)PyArray_IterNew(obj1);
if (iter1 == NULL) goto fail;
iter2 = (PyArrayIterObject *)PyArray_IterNew(obj2);
if (iter2 == NULL) goto fail; /* assume iter1 is DECREF'd at fail */
while (iter2->index < iter2->size) {
/* process with iter1->dataptr and iter2->dataptr */
PyArray_ITER_NEXT(iter1);
PyArray_ITER_NEXT(iter2);
}
Вещание по нескольким массивам
При работе с несколькими массивами вы можете захотеть использовать те же правила вещания, что и при математических операциях (т. е. ufunc). Это можно легко сделать с помощью PyArrayMultiIterObject. Это объект, возвращаемый командой Python numpy.broadcast, и его почти так же просто использовать из C. Функция PyArray_MultiIterNew ( n, ... ) используется (с объектами ввода n вместо ... ). Объекты ввода могут быть массивами или чем-либо, что может быть преобразовано в массив. Возвращается указатель на PyArrayMultiIterObject. Вещание уже выполнено, что настраивает итераторы так, что для перехода к следующему элементу в каждом массиве достаточно вызвать PyArray_ITER_NEXT для каждого из входных данных. Это инкрементирование автоматически выполняется макросом PyArray_MultiIter_NEXT ( obj ) (который может обрабатывать многоитератор obj как PyArrayMultiObject * или PyObject *). Данные из входного объекта номер i доступны с помощью PyArray_MultiIter_DATA ( obj, i ), а общий (вещаемый) размер — с помощью PyArray_MultiIter_SIZE ( obj). Ниже приведен пример использования этой функции.
mobj = PyArray_MultiIterNew(2, obj1, obj2);
size = PyArray_MultiIter_SIZE(obj);
while(size--) {
ptr1 = PyArray_MultiIter_DATA(mobj, 0);
ptr2 = PyArray_MultiIter_DATA(mobj, 1);
/* code using contents of ptr1 and ptr2 */
PyArray_MultiIter_NEXT(mobj);
}
Функция PyArray_RemoveSmallest ( multi ) может использоваться для изменения многоитератора, чтобы итерирование не производилось по измерению с наибольшим размером (это измерение устанавливается в размер 1). Код, использующий указатели, скорее всего, также потребует данных о шагах для каждого из итераторов. Эта информация хранится в multi->iters[i]->strides.
В исходном коде NumPy есть несколько примеров использования многоитератора, так как он упрощает написание N-мерного кода вещания.
Пользовательские типы данных
NumPy поставляется с 24 встроенными типами данных. Хотя этого достаточно для большинства случаев использования, возможно, пользователю понадобится дополнительный тип данных. Есть некоторая поддержка добавления дополнительного типа данных в систему NumPy. Этот дополнительный тип данных будет вести себя как обычный тип данных, за исключением того, что ufunc должны иметь зарегистрированные циклы для его обработки отдельно. Также проверка того, можно ли безопасно преобразовать другие типы данных в этот новый тип или из него, всегда вернет «можно преобразовать», если вы не зарегистрируете типы, в которые и из которых можно преобразовать ваш новый тип данных.
Исходный код NumPy содержит пример пользовательского типа данных в составе набора тестов. Файл _rational_tests.c.src в каталоге исходного кода numpy/numpy/core/src/umath/ содержит реализацию типа данных, представляющего рациональное число как отношение двух 32-битных целых чисел.
Добавление нового типа данных
Чтобы начать использовать новый тип данных, необходимо сначала определить новый тип Python для хранения скаляров нового типа данных. Приемлемо наследовать от одного из скаляров массива, если ваш новый тип имеет двоично совместимую структуру. Это позволит вашему новому типу данных иметь методы и атрибуты скаляров массива. Новые типы данных должны иметь фиксированный размер памяти (если вы хотите определить тип данных, который требует гибкого представления, например, число с переменной точностью, то используйте указатель на объект в качестве типа данных). Структура памяти объекта для нового типа Python должна содержать PyObject_HEAD, за которым следует фиксированный размер памяти, необходимый для типа данных. Например, подходящая структура для нового типа Python:
typedef struct {
PyObject_HEAD;
some_data_type obval;
/* the name can be whatever you want */
} PySomeDataTypeObject;
После определения нового объекта типа Python, необходимо определить новую структуру PyArray_Descr, члены которой содержат указатель на тип данных, который вы только что определили. Кроме того, необходимо определить требуемые функции в члене «.f»: nonzero, copyswap, copyswapn, setitem, getitem и cast. Чем больше функций в члене «.f» вы определите, тем полезнее будет новый тип данных. Очень важно инициализировать неиспользуемые функции значением NULL. Это можно сделать с помощью PyArray_InitArrFuncs (f).
После создания и заполнения структуры PyArray_Descr необходимой информацией и полезными функциями, вызывается PyArray_RegisterDataType (new_descr). Возвращаемое значение — целое число, предоставляющее уникальный номер типа, определяющий ваш тип данных. Этот номер типа должен храниться и предоставляться модулем, чтобы другие модули могли использовать его для распознавания вашего типа данных (другой механизм для нахождения номера пользовательского типа данных — поиск по имени объекта типа, связанного с типом данных, используя PyArray_TypeNumFromName ).
Регистрация функции преобразования
Вы можете преобразовывать встроенные (и другие пользовательские) типы данных в ваш тип данных автоматически. Для этого необходимо зарегистрировать функцию преобразования с типом данных, из которого вы хотите преобразовывать. Это требует написания функций низкого уровня для каждого преобразования, которое вы хотите поддерживать, и регистрации этих функций с описателем типа данных. Функция низкого уровня преобразования имеет следующий сигнатуру.
-
void castfunc(void* from, void* to, npy_intp n, void* fromarr, void* toarr) -
Преобразует элементы
nтипаfromв другой типto. Данные для преобразования находятся в непрерывном, правильно отформатированном и выровненном куске памяти, на который указывает from. Буфер для преобразования также непрерывный, правильно отформатированный и выровненный. Аргументы fromarr и toarr должны использоваться только для массивов с гибким размером элементов (строки, юникодовые строки, void).
Пример castfunc:
static void
double_to_float(double *from, float* to, npy_intp n,
void* ignore1, void* ignore2) {
while (n--) {
(*to++) = (double) *(from++);
}
}
Затем это можно зарегистрировать для преобразования двойных чисел в числа с плавающей точкой, используя код:
doub = PyArray_DescrFromType(NPY_DOUBLE);
PyArray_RegisterCastFunc(doub, NPY_FLOAT,
(PyArray_VectorUnaryFunc *)double_to_float);
Py_DECREF(doub);
Регистрация правил приведения
По умолчанию все пользовательские типы данных не предполагаются безопасно приводимыми к встроенным типам данных. Кроме того, встроенные типы данных не предполагаются безопасно приводимыми к пользовательским типам данных. Это ограничивает возможность участия пользовательских типов данных в системе приведения, используемой ufunc и другими операциями, когда происходит автоматическое приведение в NumPy. Это можно изменить, зарегистрировав типы данных как безопасно приводимые из определенного объекта типа данных. Функция PyArray_RegisterCanCast (from_descr, totype_number, scalarkind) должна использоваться для указания того, что объект типа данных from_descr может быть приведен к типу данных с номером типа totype_number. Если вы не пытаетесь изменить правила приведения скаляров, используйте NPY_NOSCALAR для аргумента scalarkind.
Если вы хотите, чтобы ваш новый тип данных также участвовал в правилах приведения скаляров, вам нужно указать функцию scalarkind в члене «.f» объекта типа данных, чтобы она возвращала вид скаляра, который должен рассматриваться как новый тип данных (значение скаляра доступно этой функции). Затем вы можете зарегистрировать типы данных, которые могут быть преобразованы, по отдельности для каждого вида скаляра, который может быть возвращен вашим пользовательским типом данных. Если вы не регистрируете обработку приведения скаляров, все ваши пользовательские типы данных будут рассматриваться как NPY_NOSCALAR.
Регистрация цикла ufunc
Вы также можете зарегистрировать циклы ufunc низкого уровня для вашего типа данных, чтобы массив ndarray вашего типа данных мог беспрепятственно применять математические операции к нему. Регистрация нового цикла с точно таким же сигнатурой arg_types молча заменяет любые ранее зарегистрированные циклы для этого типа данных.
Прежде чем вы сможете зарегистрировать цикл 1-d для ufunc, ufunc должен быть предварительно создан. Затем вы вызываете PyUFunc_RegisterLoopForType (…) с информацией, необходимой для цикла. Возвращаемое значение этой функции равно 0 если процесс прошел успешно и -1 в случае ошибки.
Подтипизация ndarray в C
Одна из менее используемых функций, которая присутствует в Python с версии 2.2, — это возможность создания подтипов типов в C. Эта возможность является одной из важных причин, по которой NumPy базируется на кодовой базе Numeric, которая уже была написана на C. Подтип в C обеспечивает гораздо большую гибкость в отношении управления памятью. Создание подтипов в C не сложно, даже если вы только начинаете осваивать создание новых типов в Python. Хотя проще всего подтипировать от единственного родительского типа, подтипирование от нескольких родительских типов также возможно. Множественное наследование в C обычно менее полезно, чем в Python, потому что ограничение на подтипы Python заключается в том, что они имеют двоично-совместимую структуру памяти. Возможно, по этой причине подтипирование от единственного родительского типа несколько проще.
Все структуры C, соответствующие объектам Python, должны начинаться с PyObject_HEAD (или PyObject_VAR_HEAD). Аналогично, любой подтип должен иметь структуру C, которая начинается с точно такой же структуры памяти, как родительский тип (или все родительские типы в случае множественного наследования). Причина в том, что Python может попытаться получить доступ к члену структуры подтипа так, как будто у него есть родительская структура (то есть он преобразует указанный указатель в указатель на родительскую структуру и затем обращается к одному из его членов). Если структуры памяти не совместимы, эта попытка приведет к непредсказуемому поведению (в конечном итоге приведет к нарушению памяти и сбою программы).
Один из элементов в PyObject_HEAD — указатель на структуру объекта типа. Новый тип Python создается путем создания новой структуры объекта типа и заполнения ее функциями и указателями для описания желаемого поведения типа. Как правило, также создается новая структура C для хранения информации, специфичной для экземпляра, для каждого объекта типа. Например, &PyArray_Type — указатель на таблицу объекта типа для ndarray, а переменная PyArrayObject * — указатель на конкретный экземпляр ndarray (одним из членов структуры ndarray является, в свою очередь, указатель на таблицу объекта типа &PyArray_Type). Наконец, PyType_Ready (<указатель_на_объект_типа>) должен быть вызван для каждого нового типа Python.
Создание подтипов
Для создания подтипа необходимо следовать аналогичной процедуре, за исключением случаев, когда требуется новое поведение, требующее новых записей в структуру объекта типа. Все остальные записи могут быть NULL и будут заполнены PyType_Ready соответствующими функциями из родительского типа(ов). В частности, для создания подтипа в C следуйте этим шагам:
-
При необходимости создайте новую структуру C для обработки каждого экземпляра вашего типа. Типичная структура C будет:
typedef _new_struct { PyArrayObject base; /* new things here */ } NewArrayObject;Обратите внимание, что полная PyArrayObject используется в качестве первого элемента, чтобы гарантировать, что двоичная структура экземпляров нового типа идентична PyArrayObject.
- Заполните новую структуру объекта типа Python указателями на новые функции, которые будут переопределять поведение по умолчанию, оставляя незаполненными (или NULL) функции, которые должны остаться прежними. Элемент tp_name должен быть другим.
- Заполните член tp_base новой структуры объекта типа указателем на родительский (основной) объект типа. Для множественного наследования также заполните член tp_bases кортежем, содержащим все родительские объекты в порядке, в котором они должны использоваться для определения наследования. Помните, что все родительские типы должны иметь одинаковую структуру C для правильной работы множественного наследования.
- Вызовите
PyType_Ready(<указатель_на_новый_тип>). Если эта функция возвращает отрицательное число, произошла ошибка, и тип не инициализирован. В противном случае тип готов к использованию. Обычно важно поместить ссылку на новый тип в словарь модуля, чтобы к нему можно было получить доступ из Python.
Дополнительную информацию о создании подтипов в C можно получить, прочитав PEP 253 (доступен по адресу https://www.python.org/dev/peps/pep-0253).
Особенности подтипирования ndarray
Некоторые специальные методы и атрибуты используются массивами для облегчения взаимодействия подтипов с базовым типом ndarray.
Метод __array_finalize__
-
ndarray.__array_finalize__ -
Некоторые функции создания массивов ndarray позволяют указать конкретный подтип, который необходимо создать. Это позволяет подтипам обрабатываться беспрепятственно во многих процедурах. Однако, когда подтип создается таким образом, ни метод __new__, ни метод __init__ не вызываются. Вместо этого подтип выделяется, и соответствующие члены структуры экземпляра заполняются. Наконец, проверяется атрибут
__array_finalize__в словаре объекта. Если он присутствует и не равен None, то он может быть CObject, содержащим указатель наPyArray_FinalizeFunc, или методом, принимающим один аргумент (который может быть None).Если атрибут
__array_finalize__является CObject, то указатель должен указывать на функцию со следующей сигнатурой:(int) (PyArrayObject *, PyObject *)
Первый аргумент — это вновь созданный подтип. Второй аргумент (если он не NULL) — это «родительский» массив (если массив был создан с помощью срезов или какой-либо другой операции, где существует чётко различимый родитель). Эта процедура может выполнять любые действия. Она должна возвращать -1 при ошибке и 0 в противном случае.
Если атрибут
__array_finalize__не равен None и не является CObject, то это должен быть Python-метод, принимающий родительский массив в качестве аргумента (который может быть None, если родительского массива нет), и не возвращающий ничего. Ошибки в этом методе будут перехвачены и обработаны.
Атрибут __array_priority__
-
ndarray.__array_priority__ -
Этот атрибут позволяет просто и гибко определять, какой подтип следует считать «основным», когда возникает операция, вовлекая два или более подтипа. В операциях, где используются разные подтипы, подтип с наибольшим атрибутом
__array_priority__определит подтип выходного/ых значений. Если два подтипа имеют одинаковый атрибут__array_priority__, то подтип первого аргумента определяет выходной подтип. По умолчанию атрибут__array_priority__возвращает значение 0,0 для базового типа ndarray и 1,0 для подтипа. Этот атрибут также может быть определен объектами, которые не являются подтипами ndarray, и может использоваться для определения, какой метод__array_wrap__должен быть вызван для выходного значения.
Метод __array_wrap__
-
ndarray.__array_wrap__ -
Любой класс или тип может определить этот метод, который должен принимать ndarray в качестве аргумента и возвращать экземпляр типа. Его можно рассматривать как противоположность методу
__array__. Этот метод используется функциями ufuncs (и другими функциями NumPy) для возможности передачи других объектов. Для Python >2.4 он также может быть использован для написания декоратора, преобразующего функцию, работающую только с ndarray, в функцию, работающую с любым типом с методами__array__и__array_wrap__.
© 2005–2021 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.20/user/c-info.beyond-basics.html