Spec-Zone.ru › NumPy 1.19

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

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

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

Одним из распространённых алгоритмических требований является возможность обхода всех элементов многомерного массива. Объект итератора массива делает это лёгким и универсальным способом, работающим для массивов любой размерности. Естественно, если вы знаете количество используемых измерений, то всегда можно написать вложенные циклы 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. Если вы уже знаете, что ваш массив непрерывный (Фортран или 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 находится в измерении \textrm{dim}^{\textrm{th}}. Если 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, члены typeobject которой будут содержать указатель на тип данных, который вы только что определили. Кроме того, должны быть определены необходимые функции в члене “.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 молча заменяет все ранее зарегистрированные циклы для этого типа данных.

Прежде чем вы сможете зарегистрировать цикл 1D для 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 новой структуры объекта типа указателем на (основной) родительский объект типа. Для множественного наследования также заполните член 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–2020 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.19/user/c-info.beyond-basics.html

Spec-Zone.ru

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