Spec-Zone.ru › NumPy 1.18

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

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

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

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

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

После обработки данных в текущем элементе массива следующий элемент массива можно получить с помощью макроса PyArray_ITER_NEXT ( iter ). Итерация всегда выполняется в стиле C (индекс последнего измерения изменяется быстрее всего). PyArray_ITER_GOTO ( iter, destination ) может использоваться для перехода к определенной точке в массиве, где destination — массив типа данных npy_intp, имеющий место для обработки по крайней мере количества измерений в базовом массиве. Иногда полезно использовать PyArray_ITER_GOTO1D ( iter, index ), который перейдет к индексу 1-го измерения, заданному значением 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 ) не может быть использован (поэтому индексация flat также не будет работать, если вы передадите этот объект обратно в Python — поэтому этого делать не следует). Обратите внимание, что возвращаемый этим вызовом объект все равно обычно приводится к типу PyArrayIterObject *. Все, что сделано, — это изменение шагов и размеров возвращаемого итератора для моделирования итерации по массиву[…,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);
}

Расширение по нескольким массивам

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

Исходный код 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, выполните следующие шаги:

  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.18/user/c-info.beyond-basics.html

Spec-Zone.ru

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