Spec-Zone.ru › NumPy 1.21

За пределами основ

Итерация по элементам массива

Базовая итерация

Одной из распространённых алгоритмических потребностей является возможность обхода всех элементов многомерного массива. Объект итератора массива позволяет легко выполнить это в общем случае, который работает для массивов любой размерности. Естественно, если вы знаете количество измерений, которые будете использовать, то всегда можно написать вложенные циклы for для выполнения итерации. Однако, если вы хотите написать код, который работает с любым количеством измерений, то вы можете использовать итератор массива. Объект итератора массива возвращается при обращении к атрибуту .flat массива.

Базовое использование заключается в вызове PyArray_IterNew ( array ), где array — объект ndarray (или один из его подклассов). Возвращаемый объект — это объект итератора массива (тот же объект, что и возвращаемый атрибутом .flat объекта ndarray). Этот объект обычно приводится к типу PyArrayIterObject*, чтобы можно было получить доступ к его членам. Единственными необходимыми членами являются iter->size , содержащий общий размер массива, iter->index, содержащий текущий одномерный индекс в массиве, и iter->dataptr , являющийся указателем на данные для текущего элемента массива. Иногда также полезно получить доступ к iter->ao , который является указателем на базовый объект ndarray.

После обработки данных в текущем элементе массива, можно получить следующий элемент массива, используя макрос PyArray_ITER_NEXT ( iter ). Итерация всегда происходит в непрерывном порядке по стилю C (последний индекс изменяется быстрее всего). 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*. Всё, что было сделано, — это модификация шагов и измерений возвращённого итератора для имитации итерации по массиву […, 0, …], где 0 помещён на dim-ое измерение. Если 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);
}

Вещание по нескольким массивам

Когда несколько массивов участвуют в операции, вы можете захотеть использовать те же правила вещания, что и математические операции (т.е. ufuncs). Это легко сделать, используя 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. Этот дополнительный тип данных будет вести себя очень похоже на обычный тип данных, за исключением того, что ufuncs должны иметь зарегистрированные одномерные циклы для его отдельной обработки. Также проверка того, могут ли другие типы данных быть преобразованы «безопасно» в этот новый тип или из него, всегда вернёт «можно преобразовать», если вы также не зарегистрируете, к каким типам ваш новый тип данных может быть преобразован и из каких.

Исходный код 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).

Регистрация функции преобразования

Вы можете захотеть преобразовывать встроенные (и другие пользовательские) типы данных в ваш тип данных автоматически. Для этого необходимо зарегистрировать функцию преобразования с типом данных, из которого вы хотите производить преобразование. Это требует написания функций низкого уровня для каждого преобразования, которое вы хотите поддерживать, а затем регистрации этих функций с описанием типа данных. Функция низкого уровня имеет сигнатуру.

voidcastfunc(void*from, void*to, npy_intpn, void*fromarr, void*toarr)

Преобразование n элементов from из одного типа to в другой. Данные для преобразования находятся в непрерывном, корректно снесённом и выровненном блоке памяти, на который указывает from. Буфер для преобразования также непрерывный, корректно снесённый и выровненный. Аргументы fromarr и toarr должны использоваться только для массивов с гибким размером элементов (строка, unicode, 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 следуйте этим шагам:

  1. При необходимости создайте новую структуру C для обработки каждого экземпляра вашего типа. Типичная структура C будет:

    typedef _new_struct {
        PyArrayObject base;
        /* new things here */
    } NewArrayObject;
    

    Обратите внимание, что полная PyArrayObject используется в качестве первой записи, чтобы гарантировать, что двоичная структура экземпляров нового типа идентична PyArrayObject.

  2. Заполните новую структуру объекта типа Python указателями на новые функции, которые переопределят стандартное поведение, оставив все функции, которые должны оставаться прежними, незаполненными (или NULL). Элемент tp_name должен быть другим.
  3. Заполните член tp_base новой структуры объекта типа указателем на (основной) родительский объект типа. Для множественного наследования также заполните член tp_bases кортежем, содержащим все родительские объекты в порядке их использования для определения наследования. Помните, что все родительские типы должны иметь одинаковую структуру C для корректной работы множественного наследования.
  4. Вызовите 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–2022 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.21/user/c-info.beyond-basics.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API