Как расширить NumPy#
Написание модуля расширения#
Хотя объект ndarray разработан для быстрых вычислений в Python, он также предназначен для общего использования и удовлетворения широкого спектра вычислительных потребностей. В результате, если абсолютная скорость важна, нет замены хорошо продуманному, скомпилированному циклу, специфичному для вашего приложения и оборудования. Это одна из причин, по которой numpy включает f2py, чтобы были доступны простые в использовании механизмы для связывания (простого) кода C/C++ и (произвольного) кода Fortran непосредственно в Python. Вам рекомендуется использовать и улучшать этот механизм. Цель этого раздела — не документировать этот инструмент, а документировать более базовые шаги написания модуля расширения, от которого зависит этот инструмент.
Когда модуль расширения написан, скомпилирован и установлен где-то в пути Python (sys.path), код можно импортировать в Python, как если бы это был стандартный файл Python. Он будет содержать объекты и методы, которые были определены и скомпилированы в коде C. Основные шаги для этого в Python хорошо документированы, и вы можете найти больше информации в документации самого Python, доступной онлайн по адресу www.python.org .
В дополнение к Python C-API, существует полный и богатый C-API для NumPy, позволяющий выполнять сложные манипуляции на уровне C. Однако для большинства приложений обычно используется лишь несколько вызовов API. Например, если вам нужно просто извлечь указатель на память вместе с некоторой информацией о форме, чтобы передать её другой вычислительной процедуре, вы будете использовать совсем другие вызовы, чем если вы пытаетесь создать новый тип, подобный массиву, или добавить новый тип данных для ndarrays. Эта глава документирует вызовы API и макросы, которые наиболее часто используются.
Требуемая подпрограмма#
Существует ровно одна функция, которая должна быть определена в вашем C-коде, чтобы Python мог использовать его как расширяемый модуль. Функция должна называться init{name}, где {name} — имя модуля в Python. Эта функция должна быть объявлена так, чтобы быть видимой для кода вне подпрограммы. Помимо добавления методов и констант, эта подпрограмма также должна содержать вызовы, такие как import_array()
и/или import_ufunc() в зависимости от того, какой C-API требуется. Забывание
разместить эти команды проявится как неприятный segmentation fault
(краш), как только будет вызвана любая подпрограмма C-API.
Фактически возможно иметь несколько функций init{name} в одном файле, в этом случае этот файл определит несколько модулей.
Однако есть некоторые трюки, чтобы заставить это работать правильно, и здесь это не рассматривается.
Минимальный init{name} метод выглядит так:
PyMODINIT_FUNC
init{name}(void)
{
(void)Py_InitModule({name}, mymethods);
import_array();
}
Методы mymethods должны быть массивом (обычно статически объявленным) структур PyMethodDef, которые содержат имена методов, фактические C-функции, переменную, указывающую, использует ли метод аргументы ключевых слов или нет, и строки документации. Это объясняется в следующем разделе. Если вы хотите добавить константы в модуль, то сохраните возвращаемое значение из Py_InitModule, которое является объектом модуля. Наиболее общий способ добавления элементов в модуль — получить словарь модуля с помощью PyModule_GetDict(module). Имея словарь модуля, вы можете вручную добавить в модуль всё, что угодно. Более простой способ добавления объектов в модуль — использовать один из трёх дополнительных вызовов Python C-API, которые не требуют отдельного извлечения словаря модуля. Они описаны в документации Python, но повторены здесь для удобства:
-
int PyModule_AddStringConstant(PyObject *модуль, char *имя, char *значение)#
Все три эти функции требуют модуль объект (возвращаемое значение Py_InitModule). имя это строка, которая маркирует значение в модуле. В зависимости от того, какая функция вызывается, значение аргумент является либо общим объектом (
PyModule_AddObjectкрадет ссылку на него), целочисленная константа или строковая константа.
Определение функций#
Второй аргумент, передаваемый в функцию Py_InitModule, — это структура, которая упрощает определение функций в модуле. В приведённом выше примере структура mymethods была бы определена ранее в файле (обычно прямо перед подпрограммой init{name}) чтобы:
static PyMethodDef mymethods[] = {
{ nokeywordfunc,nokeyword_cfunc,
METH_VARARGS,
Doc string},
{ keywordfunc, keyword_cfunc,
METH_VARARGS|METH_KEYWORDS,
Doc string},
{NULL, NULL, 0, NULL} /* Sentinel */
}
Каждая запись в массиве mymethods является PyMethodDef структура,
содержащая 1) имя Python, 2) C-функцию, реализующую
функцию, 3) флаги, указывающие, принимаются ли ключевые слова для
этой функции, и 4) строку документации для функции. Любое количество
функций может быть определено для одного модуля путём добавления дополнительных записей в
эту таблицу. Последняя запись должна быть полностью NULL, как показано, чтобы служить
сигналом. Python ищет эту запись, чтобы знать, что все
функции для модуля определены.
Последнее, что необходимо сделать для завершения модуля расширения, — это написать код, выполняющий нужные функции. Существует два вида функций: те, которые не принимают аргументы ключевых слов, и те, которые принимают.
Функции без аргументов ключевых слов#
Функции, которые не принимают аргументы ключевых слов, должны быть записаны как:
static PyObject*
nokeyword_cfunc (PyObject *dummy, PyObject *args)
{
/* convert Python arguments */
/* do function */
/* return something */
}
Фиктивный аргумент не используется в этом контексте и может быть безопасно проигнорирован. args аргумент содержит все аргументы, переданные
в функцию в виде кортежа. Вы можете делать что угодно на этом
этапе, но обычно самый простой способ управлять входными аргументами — это
вызвать PyArg_ParseTuple (args, format_string,
addresses_to_C_variables…) или PyArg_UnpackTuple (tuple, "name",
min, max, …). Хорошее описание использования первой функции
содержится в справочнике Python C-API в разделе 5.5
(Разбор аргументов и построение значений). Особое внимание
следует уделить формату "O&", который использует функции преобразования для перехода
между объектом Python и объектом C. Все остальные форматы
можно (в основном) рассматривать как частные случаи этого общего
правила. В C-API NumPy определено несколько функций преобразования,
которые могут быть полезны. В частности, PyArray_DescrConverter
функция очень полезна для поддержки произвольной спецификации типа данных. Эта функция преобразует любой допустимый объект типа данных Python в
PyArray_Descr* объект. Не забудьте передать адрес
C-переменных, которые должны быть заполнены.
Существует множество примеров использования PyArg_ParseTuple
во всем исходном коде NumPy. Стандартное использование выглядит так:
PyObject *input;
PyArray_Descr *dtype;
if (!PyArg_ParseTuple(args, "OO&", &input,
PyArray_DescrConverter,
&dtype)) return NULL;
Важно помнить, что вы получаете заимствованный ссылка на
объект при использовании строки формата "O". Однако функции
преобразования обычно требуют некоторой формы управления памятью. В этом
примере, если преобразование успешно, dtype будет содержать новую ссылку на PyArray_Descr* объект, в то время как входные данные будет содержать
заимствованную ссылку. Поэтому, если это преобразование смешать с
другим преобразованием (например, в целое число) и преобразование типа данных
будет успешным, но преобразование в целое число не удастся, то вам нужно
будет освободить счетчик ссылок на объект типа данных перед
возвратом. Типичный способ сделать это — установить dtype to NULL
перед вызовом PyArg_ParseTuple а затем использовать Py_XDECREF
на dtype перед возвратом.
После обработки входных аргументов пишется код, который фактически выполняет
работу (вероятно, вызывая другие функции по мере необходимости).
Финальный шаг C-функции — вернуть что-либо. Если возникает ошибка, то NULL должно быть возвращено (убедившись, что ошибка
действительно установлена). Если ничего не должно возвращаться, то увеличьте
Py_None и вернуть его. Если должен быть возвращён один объект, то он возвращается (убедившись, что у вас есть ссылка на него). Если должно быть возвращено несколько объектов, то нужно вернуть кортеж. Py_BuildValue функция (format_string, c_variables…) упрощает
создание кортежей объектов Python из переменных C. Обратите
особое внимание на разницу между 'N' и 'O' в строке формата,
иначе можно легко создать утечки памяти. Строка формата 'O'
увеличивает счётчик ссылок PyObject* C-переменной, которой
она соответствует, в то время как строка формата 'N' заимствует ссылку на соответствующую PyObject* C-переменная. Вы должны использовать 'N', если вы уже
создали ссылку на объект и просто хотите передать эту
ссылку в кортеж. Вы должны использовать 'O', если у вас есть только заимствованная
ссылка на объект и нужно создать одну для предоставления в кортеж.
Функции с аргументами-ключевыми словами#
Эти функции очень похожи на функции без ключевых аргументов. Единственное отличие заключается в том, что сигнатура функции:
static PyObject*
keyword_cfunc (PyObject *dummy, PyObject *args, PyObject *kwds)
{
...
}
Аргумент kwds содержит словарь Python, ключи которого — это имена аргументов ключевых слов, а значения — соответствующие значения аргументов ключевых слов. Этот словарь можно обрабатывать как угодно. Однако самый простой способ работы с ним — заменить
PyArg_ParseTuple (args, format_string, addresses…) функция с
вызовом PyArg_ParseTupleAndKeywords (args, kwds, format_string, char *kwlist[], addresses…). Параметр kwlist этой функции является NULL -завершающий массив строк, предоставляющий ожидаемые
именованные аргументы. Должна быть одна строка для каждой записи в
format_string. Использование этой функции вызовет TypeError, если переданы
недопустимые именованные аргументы.
Для получения дополнительной помощи по этой функции см. раздел 1.8 (Параметры-ключевые слова для функций расширения) руководства по расширению и встраиванию в документации Python.
Подсчёт ссылок#
Основная сложность при написании модулей расширения — подсчёт ссылок. Это важная причина популярности f2py, weave, Cython, ctypes и т.д. Если неправильно обрабатывать счётчики ссылок, можно столкнуться с проблемами от утечек памяти до ошибок сегментации. Единственная стратегия, которую я знаю для правильной обработки счётчиков ссылок, — это кровь, пот и слёзы. Сначала вы вбиваете себе в голову, что каждая переменная Python имеет счётчик ссылок. Затем вы точно понимаете, что каждая функция делает со счётчиком ссылок ваших объектов, чтобы правильно использовать DECREF и INCREF, когда они нужны. Подсчёт ссылок действительно может проверить ваше терпение и усердие в программировании. Несмотря на мрачное описание, большинство случаев подсчёта ссылок довольно просты, и самая частая трудность — не использовать DECREF для объектов перед досрочным выходом из процедуры из-за какой-либо ошибки. На втором месте — распространённая ошибка не владения ссылкой на объект, который передаётся в функцию или макрос, который собирается украсть ссылку ( например, PyTuple_SET_ITEM, и
большинство функций, которые принимают PyArray_Descr объектов).
Обычно вы получаете новую ссылку на переменную, когда она создаётся или является возвращаемым значением некоторой функции (хотя есть некоторые заметные исключения — например, получение элемента из кортежа или словаря). Когда вы владеете ссылкой, вы ответственны за то, чтобы убедиться, что Py_DECREF (var) вызывается, когда переменная больше
не нужна (и никакая другая функция не "украла" её
ссылку). Также, если вы передаёте объект Python в функцию,
которая "украдёт" ссылку, то вам нужно убедиться, что вы владеете им
(или используйте Py_INCREF чтобы получить собственную ссылку). Вы также столкнетесь с понятием заимствования ссылки. Функция, которая заимствует ссылку, не изменяет счетчик ссылок объекта и не ожидает "удерживать" ссылку. Она просто временно использует объект. Когда вы используете PyArg_ParseTuple или
PyArg_UnpackTuple вы получаете заимствованную ссылку на объекты в кортеже и не должны изменять их счётчик ссылок внутри вашей функции. С практикой вы можете научиться правильно работать со счётчиком ссылок, но поначалу это может быть разочаровывающим.
Одним из распространенных источников ошибок подсчета ссылок является Py_BuildValue
функции. Внимательно обратите внимание на разницу между символом формата 'N' и символом формата 'O'. Если вы создаете новый объект в своей подпрограмме (например, выходной массив) и передаете его обратно в кортеже возвращаемых значений, то, скорее всего, следует использовать символ формата 'N' в Py_BuildValue. Символ ‘O’
увеличит счётчик ссылок на единицу. Это оставит
вызывающую сторону с двумя счётчиками ссылок для совершенно нового массива. Когда
переменная удалена и счётчик ссылок уменьшен на единицу, всё ещё
будет этот дополнительный счётчик ссылок, и массив никогда не будет
освобождён. У вас будет утечка памяти, вызванная подсчётом ссылок.
Использование символа ‘N’ позволит избежать этой ситуации, так как он вернёт
вызывающей стороне объект (внутри кортежа) с единичным счётчиком ссылок.
Работа с объектами массивов#
Большинству модулей расширения для NumPy потребуется доступ к памяти объекта ndarray (или одного из его подклассов). Самый простой способ сделать это не требует глубоких знаний о внутреннем устройстве NumPy. Метод заключается в
Убедитесь, что вы работаете с корректным массивом (выровненным, в порядке байтов машины и односегментным) правильного типа и количества измерений.
Путем преобразования из некоторого объекта Python с использованием
PyArray_FromAnyили макрос, построенный на нем.Путем создания нового ndarray желаемой формы и типа с использованием
PyArray_NewFromDescrили более простой макрос или функция на его основе.
Получить форму массива и указатель на его фактические данные.
Передать данные и информацию о форме в подпрограмму или другой раздел кода, который фактически выполняет вычисления.
Если вы пишете алгоритм, то я рекомендую использовать информацию о шаге, содержащуюся в массиве, для доступа к элементам массива (
PyArray_GetPtrмакросы делают это безболезненным). Затем, вы можете ослабить требования, чтобы не принуждать к односегментному массиву и возможному копированию данных.
Каждый из этих подразделов рассматривается в следующих подразделах.
Преобразование произвольного объекта последовательности#
Основная процедура для получения массива из любого объекта Python, который может быть преобразован в массив, это PyArray_FromAny. Эта
функция очень гибкая с множеством входных аргументов. Несколько макросов
облегчают использование базовой функции. PyArray_FROM_OTF является, пожалуй, наиболее полезным из этих макросов для наиболее распространенных случаев использования. Он позволяет преобразовать произвольный объект Python в массив определенного встроенного типа данных ( например, float), при указании определённого набора требований ( например, непрерывный, выровненный и
доступный для записи). Синтаксис
PyArray_FROM_OTFВозвращает ndarray из любого объекта Python, obj, который может быть преобразован в массив. Количество измерений в возвращаемом массиве определяется объектом. Желаемый тип данных возвращаемого массива указан в typenum который должен быть одним из перечисленных типов. The требования для возвращаемого массива может быть любая комбинация стандартных флагов массива. Каждый из этих аргументов объясняется более подробно ниже. Вы получаете новую ссылку на массив при успехе. При неудаче,
NULLвозвращается, и устанавливается исключение.- obj
Объектом может быть любой объект Python, преобразуемый в ndarray. Если объект уже является (подклассом) ndarray, который удовлетворяет требованиям, то возвращается новая ссылка. В противном случае создается новый массив. Содержимое obj копируются в новый массив, если не используется интерфейс массива, чтобы данные не нужно было копировать. Объекты, которые могут быть преобразованы в массив, включают: 1) любой вложенный объект последовательности, 2) любой объект, предоставляющий интерфейс массива, 3) любой объект с атрибутом
__array__метод (который должен возвращать ndarray), и 4) любой скалярный объект (превращается в нульмерный массив). Подклассы ndarray, которые в остальном соответствуют требованиям, будут пропущены. Если вы хотите гарантировать базовый класс ndarray, используйтеNPY_ARRAY_ENSUREARRAYв флаге требований. Копия создается только при необходимости. Если вы хотите гарантировать копию, передайтеNPY_ARRAY_ENSURECOPYк флагу требований.- typenum
Один из перечисленных типов или
NPY_NOTYPEесли тип данных должен быть определен из самого объекта. Можно использовать имена на основе C:В качестве альтернативы можно использовать имена битовой ширины, поддерживаемые на платформе. Например:
Объект будет преобразован в желаемый тип только если это можно сделать без потери точности. В противном случае
NULLбудет возвращено, и будет вызвана ошибка. ИспользуйтеNPY_ARRAY_FORCECASTв требованиях флага, чтобы переопределить это поведение.- требования
Модель памяти для ndarray допускает произвольные шаги в каждом измерении для перехода к следующему элементу массива. Однако часто вам нужно взаимодействовать с кодом, который ожидает C-смежное или Fortran-смежное расположение памяти. Кроме того, ndarray может быть невыровненным (адрес элемента не является целым кратным размера элемента), что может привести к сбою вашей программы (или, по крайней мере, к более медленной работе), если вы попытаетесь разыменовать указатель на данные массива. Обе эти проблемы можно решить, преобразовав объект Python в массив, который более «корректно ведёт себя» для вашего конкретного использования.
Флаг требований позволяет указать, какой тип массива допустим. Если переданный объект не удовлетворяет этим требованиям, создаётся копия, чтобы возвращаемый объект удовлетворял требованиям. Эти ndarray могут использовать очень общий указатель на память. Этот флаг позволяет указать желаемые свойства возвращаемого объекта массива. Все флаги объясняются в подробной главе API. Наиболее часто нужные флаги — это
NPY_ARRAY_IN_ARRAY,NPY_ARRAY_OUT_ARRAY, иNPY_ARRAY_INOUT_ARRAY:NPY_ARRAY_IN_ARRAYЭтот флаг полезен для массивов, которые должны быть в C-непрерывном порядке и выровнены. Такие массивы обычно являются входными массивами для некоторых алгоритмов.
NPY_ARRAY_OUT_ARRAYЭтот флаг полезен для указания массива, который находится в C-последовательном порядке, выровнен и может быть записан. Такой массив обычно возвращается как выходной (хотя обычно такие выходные массивы создаются с нуля).
NPY_ARRAY_INOUT_ARRAYЭтот флаг полезен для указания массива, который будет использоваться как для ввода, так и для вывода.
PyArray_ResolveWritebackIfCopyдолжен быть вызван передPy_DECREFв конце процедуры интерфейса для обратной записи временных данных в исходный переданный массив. ИспользованиеNPY_ARRAY_WRITEBACKIFCOPYФлаг требует, чтобы входной объект уже был массивом (поскольку другие объекты не могут быть автоматически обновлены таким образом). Если произойдет ошибка, используйтеPyArray_DiscardWritebackIfCopy(obj) на массиве с установленными этими флагами. Это сделает базовый массив доступным для записи без копирования содержимого обратно в исходный массив.
Другие полезные флаги, которые можно объединять через ИЛИ как дополнительные требования:
NPY_ARRAY_FORCECASTПривести к желаемому типу, даже если это невозможно без потери информации.
NPY_ARRAY_ENSURECOPYУбедитесь, что результирующий массив является копией оригинала.
NPY_ARRAY_ENSUREARRAYУбедитесь, что результирующий объект является фактически ndarray, а не подклассом.
Примечание
Определение того, является ли массив байт-свопнутым, зависит от типа данных массива. Массивы с собственным порядком байтов всегда запрашиваются через PyArray_FROM_OTF и поэтому нет необходимости в NPY_ARRAY_NOTSWAPPED флаг в аргументе requirements. Также нет возможности получить массив с обратным порядком байтов из этой процедуры.
Создание совершенно нового ndarray#
Индексы для взятия вдоль каждого одномерного среза PyArray_NewFromDescr. Все функции
создания массивов проходят через этот часто используемый код. Из-за
его гибкости, его использование может быть несколько запутанным. В результате
существуют более простые формы, которые легче использовать. Эти формы являются частью
PyArray_SimpleNew семейство функций, которые упрощают интерфейс, предоставляя значения по умолчанию для распространённых случаев использования.
Доступ к памяти ndarray и элементам ndarray#
Если obj — ndarray (PyArrayObject*), то область данных
ndarray указывается указателем void* PyArray_DATA (obj) или указатель char* PyArray_BYTES (obj). Помните, что (в общем случае)
эта область данных может быть не выровнена в соответствии с типом данных, может
представлять данные с обратным порядком байтов и/или может быть недоступна для записи. Если
область данных выровнена и имеет нативный порядок байтов, то способ доступа к
конкретному элементу массива определяется только массивом
переменных npy_intp, PyArray_STRIDES (obj). В частности, этот
c-массив целых чисел показывает, сколько байты должно быть добавлено к текущему указателю элемента, чтобы перейти к следующему элементу в каждом измерении. Для массивов размерностью менее 4 есть PyArray_GETPTR{k}
(obj, …) макросы, где {k} — целое число 1, 2, 3 или 4, которые упрощают использование шагов массива. Аргументы … представляют {k} неотрицательных целочисленных индексов в массиве. Например, предположим E является 3-мерным ndarray. Указатель (void*) на элемент E[i,j,k]
получается как PyArray_GETPTR3 (E, i, j, k).
Как объяснялось ранее, C-стиль непрерывных массивов и Fortran-стиль непрерывных массивов имеют определенные шаблоны шага. Два флага массива (NPY_ARRAY_C_CONTIGUOUS и NPY_ARRAY_F_CONTIGUOUS) указывают,
соответствует ли шаблон шага конкретного массива
C-стилю непрерывности или фортран-стилю непрерывности или ни тому, ни другому. Соответствует ли
шаблон шага стандартному C или Fortran, можно
проверить с помощью PyArray_IS_C_CONTIGUOUS (obj) и
PyArray_ISFORTRAN (obj) соответственно. Большинство сторонних библиотек ожидают непрерывные массивы. Однако часто не сложно поддерживать общее страйдинг. Я рекомендую вам использовать информацию о страйдинге в вашем собственном коде, когда это возможно, и оставлять требования к односегментности для обертывания стороннего кода. Использование информации о страйдинге, предоставляемой с ndarray, вместо требования непрерывного страйдинга уменьшает необходимое копирование.
Пример#
Следующий пример показывает, как можно написать обёртку, которая принимает два входных аргумента (которые будут преобразованы в массив) и один выходной аргумент (который должен быть массивом). Функция возвращает None и обновляет выходной массив. Обратите внимание на обновлённое использование семантики WRITEBACKIFCOPY для NumPy версии 1.14 и выше
static PyObject *
example_wrapper(PyObject *dummy, PyObject *args)
{
PyObject *arg1=NULL, *arg2=NULL, *out=NULL;
PyObject *arr1=NULL, *arr2=NULL, *oarr=NULL;
if (!PyArg_ParseTuple(args, "OOO!", &arg1, &arg2,
&PyArray_Type, &out)) return NULL;
arr1 = PyArray_FROM_OTF(arg1, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY);
if (arr1 == NULL) return NULL;
arr2 = PyArray_FROM_OTF(arg2, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY);
if (arr2 == NULL) goto fail;
#if NPY_API_VERSION >= 0x0000000c
oarr = PyArray_FROM_OTF(out, NPY_DOUBLE, NPY_ARRAY_INOUT_ARRAY2);
#else
oarr = PyArray_FROM_OTF(out, NPY_DOUBLE, NPY_ARRAY_INOUT_ARRAY);
#endif
if (oarr == NULL) goto fail;
/* code that makes use of arguments */
/* You will probably need at least
nd = PyArray_NDIM(<..>) -- number of dimensions
dims = PyArray_DIMS(<..>) -- npy_intp array of length nd
showing length in each dim.
dptr = (double *)PyArray_DATA(<..>) -- pointer to data.
If an error occurs goto fail.
*/
Py_DECREF(arr1);
Py_DECREF(arr2);
#if NPY_API_VERSION >= 0x0000000c
PyArray_ResolveWritebackIfCopy(oarr);
#endif
Py_DECREF(oarr);
Py_INCREF(Py_None);
return Py_None;
fail:
Py_XDECREF(arr1);
Py_XDECREF(arr2);
#if NPY_API_VERSION >= 0x0000000c
PyArray_DiscardWritebackIfCopy(oarr);
#endif
Py_XDECREF(oarr);
return NULL;
}