Spec-Zone.ru › NumPy 2.0

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

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

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

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

Базовое использование заключается в вызове PyArray_IterNew ( 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), то просто добавление размера элемента к текущему указателю позволит очень эффективно перемещаться по массиву. Другими словами, такой код, вероятно, будет быстрее в непрерывном случае (в предположении, что это двойные данные).

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 ) не может быть использован (следовательно, индексирование с использованием flat не будет работать, если вы передадите этот объект в 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 как PyArrayMultiIterObject* или PyObject*. Данные из входного объекта № i доступны с помощью PyArray_MultiIter_DATA ( obj, i ). Следующий пример демонстрирует использование этой функции.

mobj = PyArray_MultiIterNew(2, obj1, obj2);
size = mobj->size;
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/_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). Значение возвращаемое этой функцией — целое число, предоставляющее вам уникальный номер типа, который определяет ваш тип данных. Этот номер типа следует сохранить и сделать доступным вашему модулю, чтобы другие модули могли использовать его для распознавания вашего типа данных.

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

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

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

Преобразовать элементы n из одного типа 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++);
    }
}

Это можно было бы зарегистрировать для преобразования double в float с помощью кода:

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 заменит любые ранее зарегистрированные циклы для этого типа данных.

Прежде чем вы сможете зарегистрировать одномерный цикл для 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). Наконец, для каждого нового типа Python необходимо вызвать PyType_Ready (<указатель_на_объект_типа>).

Создание подтипов

Для создания подтипа необходимо выполнить аналогичную процедуру, за исключением того, что новые записи в структуре объекта типа требуются только для различных типов поведения. Все остальные записи могут быть 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 новой структуры объекта Python типа указателем на (основной) родительский объект типа. Для множественного наследования также заполните член 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, то он может быть PyCapsule, содержащим указатель на PyArray_FinalizeFunc, или это может быть метод, принимающий один аргумент (который может быть None).

Если атрибут __array_finalize__ является PyCapsule, то указатель должен указывать на функцию с сигнатурой:

(int) (PyArrayObject *, PyObject *)

Первый аргумент — это вновь созданный подтип. Второй аргумент (если не NULL) — «родительский» массив (если массив был создан с помощью срезов или какой-либо другой операции, где явно присутствует родительский массив). Эта процедура может делать все, что она хочет. Она должна возвращать -1 при ошибке и 0 в противном случае.

Если атрибут __array_finalize__ не равен None и не является PyCapsule, то он должен быть методом Python, принимающим родительский массив в качестве аргумента (который может быть None, если родительский массив отсутствует), и не возвращать ничего. Ошибки в этом методе будут перехвачены и обработаны.

Атрибут __array_priority__

ndarray.__array_priority__

Этот атрибут позволяет просто, но гибко определять, какой подтип следует считать «первичным», когда возникает операция, включающая два или более подтипов. В операциях, где используются разные подтипы, подтип с наибольшим атрибутом __array_priority__ определит подтип результата(ов). Если у двух подтипов одинаковый атрибут __array_priority__, то подтип первого аргумента определяет результат. У базового типа ndarray атрибут __array_priority__ по умолчанию возвращает значение 0,0, а для подтипа — 1,0. Этот атрибут также может быть определен для объектов, которые не являются подтипами ndarray, и может использоваться для определения того, какой метод __array_wrap__ должен быть вызван для возвращаемого результата.

Метод __array_wrap__

ndarray.__array_wrap__

Любой класс или тип может определить этот метод, который должен принимать ndarray в качестве аргумента и возвращать экземпляр типа. Его можно рассматривать как противоположность методу __array__. Этот метод используется функциями ufuncs (и другими функциями NumPy) для возможности прохождения через другие объекты. Для Python >2.4 он также может использоваться для написания декоратора, который преобразует функцию, работающую только с ndarray, в функцию, работающую с любым типом с методами __array__ и __array_wrap__.

© 2005–2024 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/2.0/user/c-info.beyond-basics.html

Spec-Zone.ru

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