Помимо основ#

Путешествие открытия заключается не в поиске новых пейзажей, а в обладании
новые глаза.
Марсель Пруст
Открытие — это видеть то, что видели все, и думать то, что никто
никто другой не думал.
Альберт Сент-Дьёрди

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

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

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

Базовое использование заключается в вызове PyArray_IterNew ( array ) где array является объектом ndarray (или одним из его подклассов). Возвращаемый объект является объектом итератора массива (тем же объектом, возвращаемым атрибутом .flat ndarray). Этот объект обычно приводится к PyArrayIterObject*, чтобы можно было получить доступ к его членам. Единственные необходимые члены - это iter->size который содержит общий размер массива, iter->index, который содержит текущий 1-d индекс в массиве, и 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++;
}

Итерация по всем осям, кроме одной#

Распространённый алгоритм заключается в переборе всех элементов массива и выполнении некоторой функции с каждым элементом путём вызова функции. Поскольку вызовы функций могут быть затратными по времени, один из способов ускорить такой алгоритм — написать функцию так, чтобы она принимала вектор данных, а затем организовать итерацию так, чтобы вызов функции выполнялся для целого измерения данных за раз. Это увеличивает объём работы, выполняемой за один вызов функции, тем самым сокращая накладные расходы на вызов функции до меньшей доли общего времени. Даже если внутренняя часть цикла выполняется без вызова функции, может быть выгодно выполнять внутренний цикл по измерению с наибольшим количеством элементов, чтобы воспользоваться улучшениями скорости, доступными в микропроцессорах, использующих конвейеризацию для ускорения базовых операций.

The 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);
}

Трансляция (broadcasting) по нескольким массивам#

Когда в операции участвуют несколько массивов, вы можете использовать те же правила трансляции, что и математические операции (т.е. используют универсальные функции (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 ) может использоваться для взятия объекта multi-iterator и настройки всех итераторов так, чтобы итерация не происходила по наибольшему измерению (оно делает это измерение размером 1). Код, по которому происходит цикл и который использует указатели, скорее всего также потребует данных о шагах для каждого из итераторов. Эта информация хранится в multi->iters[i]->strides.

В исходном коде NumPy есть несколько примеров использования multi-iterator, так как он делает код N-мерного вещания очень простым для написания. Просмотрите исходный код для большего количества примеров.

Пользовательские типы данных#

NumPy поставляется с 24 встроенными типами данных. Хотя это покрывает большинство возможных случаев использования, можно представить, что пользователю может потребоваться дополнительный тип данных. Существует некоторая поддержка добавления дополнительного типа данных в систему NumPy. Этот дополнительный тип данных будет вести себя почти как обычный тип данных, за исключением того, что у ufuncs должны быть зарегистрированы 1-d циклы для его отдельной обработки. Также проверка того, могут ли другие типы данных быть приведены "безопасно" к этому новому типу или из него, всегда будет возвращать "может быть приведен", если вы также не зарегистрируете, к каким типам ваш новый тип данных может быть приведен и из каких.

Исходный код 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 структура, член typeobject которой будет содержать указатель на только что определенный тип данных. Кроме того, необходимые функции в члене ".f" должны быть определены: nonzero, copyswap, copyswapn, setitem, getitem и cast. Чем больше функций определено в члене ".f", тем полезнее будет новый тип данных. Очень важно инициализировать неиспользуемые функции как NULL. Это может быть достигнуто с помощью PyArray_InitArrFuncs (f).

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

Обратите внимание, что этот API по своей природе небезопасен для многопоточности. См. Потокобезопасность для получения дополнительной информации о потокобезопасности в NumPy.

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

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

void castfunc(void *из, 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++);
    }
}

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

Прежде чем зарегистрировать 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, то он может быть либо 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__ то подтип первого аргумента определяет вывод. По умолчанию __array_priority__ атрибут возвращает значение 0.0 для базового типа ndarray и 1.0 для подтипа. Этот атрибут также может быть определён объектами, не являющимися подтипами ndarray, и может использоваться для определения того, какой __array_wrap__ метод должен быть вызван для возвращаемого вывода.

Метод __array_wrap__#

ndarray.__array_wrap__#

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