Spec-Zone.ru › NumPy 2.0

numpy.i: файл интерфейса SWIG для NumPy

Введение

Simple Wrapper and Interface Generator (или SWIG) — мощный инструмент для генерации оберточного кода для взаимодействия с широким спектром языков сценариев. SWIG может анализировать заголовочные файлы и, используя только прототипы кода, создавать интерфейс для целевого языка. Но SWIG не всемогущ. Например, он не может определить из прототипа:

double rms(double* seq, int n);

что именно seq представляет собой. Является ли это единственным значением, которое изменяется на месте? Является ли это массивом, и если да, то какова его длина? Является ли это только входными данными? Только выходными данными? Входом-выходом? SWIG не может определить эти детали и не пытается этого сделать.

Если бы мы проектировали rms, мы, вероятно, сделали бы его процедурой, которая принимает входной массив длиной n из значений типа double, называемый seq, и возвращает среднеквадратическое отклонение. Однако стандартное поведение SWIG заключается в создании оберточной функции, которая компилируется, но практически невозможно использовать с языка сценариев так, как задумывалась процедура на C.

Для Python предпочтительным способом обработки непрерывных (или, технически, строкообразных) блоков однородных данных является NumPy, который предоставляет полный объектно-ориентированный доступ к многомерным массивам данных. Поэтому наиболее логичным интерфейсом Python для функции rms была бы (включая строку документации):

def rms(seq):
    """
    rms: return the root mean square of a sequence
    rms(numpy.ndarray) -> double
    rms(list) -> double
    rms(tuple) -> double
    """

где seq был бы массивом NumPy значений типа double, а его длина n извлекалась бы из seq внутри перед передачей в процедуру на C. Более того, так как NumPy поддерживает создание массивов из произвольных последовательностей Python, seq само по себе могло бы быть почти произвольной последовательностью (при условии, что каждый элемент может быть преобразован в double), и код обёртки преобразовывал бы его во внутренний массив NumPy перед извлечением данных и длины.

SWIG позволяет определять эти типы преобразований с помощью механизма, называемого typemaps. Этот документ содержит информацию о том, как использовать numpy.i, файл интерфейса SWIG, который определяет ряд typemaps, призванных сделать преобразования, связанные с массивами, описанными выше, относительно простыми для реализации. Например, предположим, что прототип функции rms, определённой выше, находился в заголовочном файле с именем rms.h. Чтобы получить интерфейс Python, о котором говорилось выше, ваш файл интерфейса SWIG должен содержать следующее:

%{
#define SWIG_FILE_WITH_INIT
#include "rms.h"
%}

%include "numpy.i"

%init %{
import_array();
%}

%apply (double* IN_ARRAY1, int DIM1) {(double* seq, int n)};
%include "rms.h"

Typemaps основаны на списке одного или нескольких аргументов функции, либо по типу, либо по типу и имени. Такие списки мы будем называть сигнатурами. Одна из многих typemaps, определённых в numpy.i, используется выше и имеет сигнатуру (double* IN_ARRAY1, int DIM1). Имена аргументов подразумевают, что аргумент double* является одномерным входным массивом, а int представляет собой размер этого измерения. Именно такой паттерн присутствует в прототипе rms.

Вероятнее всего, у фактических прототипов, которые необходимо обернуть, не будет имен аргументов IN_ARRAY1 и DIM1. Мы используем директиву SWIG %apply, чтобы применить typemap для одномерных входных массивов типа double к фактическому прототипу, используемому rms. Для эффективного использования numpy.i необходимо знать, какие typemaps доступны и что они делают.

Файл интерфейса SWIG, включающий вышеприведённые директивы SWIG, сгенерирует код обёртки, примерно такой:

 1 PyObject *_wrap_rms(PyObject *args) {
 2   PyObject *resultobj = 0;
 3   double *arg1 = (double *) 0 ;
 4   int arg2 ;
 5   double result;
 6   PyArrayObject *array1 = NULL ;
 7   int is_new_object1 = 0 ;
 8   PyObject * obj0 = 0 ;
 9
10   if (!PyArg_ParseTuple(args,(char *)"O:rms",&obj0)) SWIG_fail;
11   {
12     array1 = obj_to_array_contiguous_allow_conversion(
13                  obj0, NPY_DOUBLE, &is_new_object1);
14     npy_intp size[1] = {
15       -1
16     };
17     if (!array1 || !require_dimensions(array1, 1) ||
18         !require_size(array1, size, 1)) SWIG_fail;
19     arg1 = (double*) array1->data;
20     arg2 = (int) array1->dimensions[0];
21   }
22   result = (double)rms(arg1,arg2);
23   resultobj = SWIG_From_double((double)(result));
24   {
25     if (is_new_object1 && array1) Py_DECREF(array1);
26   }
27   return resultobj;
28 fail:
29   {
30     if (is_new_object1 && array1) Py_DECREF(array1);
31   }
32   return NULL;
33 }

Typemaps из numpy.i отвечают за следующие строки кода: 12–20, 25 и 30. Строка 10 анализирует входные данные для функции rms. Из формата строки "O:rms" видно, что список аргументов ожидается как единственный объект Python (указанный O перед двоеточием), указатель которого хранится в obj0. Несколько функций, предоставляемых numpy.i, вызываются для выполнения и проверки возможного преобразования от общего объекта Python к массиву NumPy. Эти функции описаны в разделе Вспомогательные функции, но, надеемся, их имена понятны. В строке 12 мы используем obj0 для создания массива NumPy. В строке 17 мы проверяем валидность результата: что он не равен нулю и что он имеет одно измерение произвольной длины. После проверки этих состояний мы извлекаем буфер данных и длину в строках 19 и 20, чтобы мы могли вызвать основную функцию C в строке 22. Строка 25 выполняет управление памятью в случае, если мы создали новый массив, который больше не нужен.

Этот код имеет значительный объём обработки ошибок. Обратите внимание, что SWIG_fail — макрос для goto fail, ссылаясь на метку в строке 28. Если пользователь предоставляет неправильное количество аргументов, это будет обнаружено в строке 10. Если создание массива NumPy завершается неудачно или создаёт массив с неправильным количеством измерений, эти ошибки будут обнаружены в строке 17. И, наконец, если ошибка обнаружена, память всё равно управляется корректно в строке 30.

Обратите внимание, что если сигнатура функции C была в другом порядке:

double rms(int n, double* seq);

то SWIG не сопоставил бы сигнатуру typemap, приведённую выше, со списком аргументов для rms. К счастью, у numpy.i есть набор typemaps, в котором указатель данных приводится последним:

%apply (int DIM1, double* IN_ARRAY1) {(int n, double* seq)};

Это просто приводит к переключению определений arg1 и arg2 в строках 3 и 4 сгенерированного кода выше и их назначений в строках 19 и 20.

Использование numpy.i

Файл numpy.i в настоящее время находится в подкаталоге tools/swig в каталоге установки numpy. Обычно вы захотите скопировать его в каталог, где вы разрабатываете свои обертки.

Простой модуль, использующий только один файл интерфейса SWIG, должен включать следующее:

%{
#define SWIG_FILE_WITH_INIT
%}
%include "numpy.i"
%init %{
import_array();
%}

Внутри скомпилированного модуля Python import_array() должен вызываться только один раз. Это может происходить в файле C/C++, который вы написали и который связан с модулем. Если это так, то ни один из ваших файлов интерфейса не должен #define SWIG_FILE_WITH_INIT или вызывать import_array(). Или этот вызов инициализации может быть в файле обёртки, сгенерированном SWIG из файла интерфейса, который имеет блок %init, как выше. Если это так, и у вас есть более одного файла интерфейса SWIG, то только один файл интерфейса должен #define SWIG_FILE_WITH_INIT и вызывать import_array().

Доступные типмапы

Директивы типмапов, предоставляемые numpy.i для массивов различных типов данных, скажем double и int, и размеров различных типов, скажем int или long, идентичны друг другу за исключением спецификаций типов C и NumPy. Поэтому типмапы реализуются (обычно за кулисами) с помощью макроса:

%numpy_typemaps(DATA_TYPE, DATA_TYPECODE, DIM_TYPE)

который можно вызвать для соответствующих (DATA_TYPE, DATA_TYPECODE, DIM_TYPE) троек. Например:

%numpy_typemaps(double, NPY_DOUBLE, int)
%numpy_typemaps(int,    NPY_INT   , int)

Интерфейсный файл numpy.i использует макрос %numpy_typemaps для реализации типмапов для следующих типов данных C и типов размеров int:

  • signed char
  • unsigned char
  • short
  • unsigned short
  • int
  • unsigned int
  • long
  • unsigned long
  • long long
  • unsigned long long
  • float
  • double

В следующих описаниях мы ссылаемся на обобщённый DATA_TYPE, который может быть любым из перечисленных выше типов данных C, и DIM_TYPE, который должен быть одним из многих типов целых чисел.

Подписи типмапов в основном различаются по имени указателя на буфер. Имена с FARRAY предназначены для массивов с фортран-порядком, а имена с ARRAY предназначены для массивов с порядком C (или одномерных массивов).

Входные массивы

Входные массивы определяются как массивы данных, которые передаются в процедуру, но не изменяются на месте или не возвращаются пользователю. Поэтому входной массив Python может быть почти любым последовательностью Python (например, списком), который можно преобразовать в требуемый тип массива. Подписи входных массивов:

Одномерные:

  • (   DATA_TYPE IN_ARRAY1[ANY] )
  • (   DATA_TYPE* IN_ARRAY1, int DIM1 )
  • (   int DIM1, DATA_TYPE* IN_ARRAY1 )

Двумерные:

  • (   DATA_TYPE IN_ARRAY2[ANY][ANY] )
  • (   DATA_TYPE* IN_ARRAY2, int DIM1, int DIM2 )
  • (   int DIM1, int DIM2, DATA_TYPE* IN_ARRAY2 )
  • (   DATA_TYPE* IN_FARRAY2, int DIM1, int DIM2 )
  • (   int DIM1, int DIM2, DATA_TYPE* IN_FARRAY2 )

Трехмерные:

  • (   DATA_TYPE IN_ARRAY3[ANY][ANY][ANY] )
  • (   DATA_TYPE* IN_ARRAY3, int DIM1, int DIM2, int DIM3 )
  • (   int DIM1, int DIM2, int DIM3, DATA_TYPE* IN_ARRAY3 )
  • (   DATA_TYPE* IN_FARRAY3, int DIM1, int DIM2, int DIM3 )
  • (   int DIM1, int DIM2, int DIM3, DATA_TYPE* IN_FARRAY3 )

Четырехмерные:

  • (DATA_TYPE IN_ARRAY4[ANY][ANY][ANY][ANY])
  • (DATA_TYPE* IN_ARRAY4, DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4)
  • (DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, , DIM_TYPE DIM4, DATA_TYPE* IN_ARRAY4)
  • (DATA_TYPE* IN_FARRAY4, DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4)
  • (DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4, DATA_TYPE* IN_FARRAY4)

Первая указанная подпись, ( DATA_TYPE IN_ARRAY[ANY] ), предназначена для одномерных массивов со жёстко заданными размерами. Аналогично, ( DATA_TYPE IN_ARRAY2[ANY][ANY] ) предназначена для двумерных массивов со жёстко заданными размерами, и так далее для трёхмерных.

Массивы на месте

Массивы на месте определяются как массивы, которые изменяются на месте. Входные значения могут использоваться или нет, но значения на момент возврата функции имеют значение. Поэтому предоставленный аргумент Python должен быть массивом NumPy требуемого типа. Подписи для массивов на месте:

Одномерные:

  • (   DATA_TYPE INPLACE_ARRAY1[ANY] )
  • (   DATA_TYPE* INPLACE_ARRAY1, int DIM1 )
  • (   int DIM1, DATA_TYPE* INPLACE_ARRAY1 )

Двумерные:

  • (   DATA_TYPE INPLACE_ARRAY2[ANY][ANY] )
  • (   DATA_TYPE* INPLACE_ARRAY2, int DIM1, int DIM2 )
  • (   int DIM1, int DIM2, DATA_TYPE* INPLACE_ARRAY2 )
  • (   DATA_TYPE* INPLACE_FARRAY2, int DIM1, int DIM2 )
  • (   int DIM1, int DIM2, DATA_TYPE* INPLACE_FARRAY2 )

Трехмерные:

  • (   DATA_TYPE INPLACE_ARRAY3[ANY][ANY][ANY] )
  • (   DATA_TYPE* INPLACE_ARRAY3, int DIM1, int DIM2, int DIM3 )
  • (   int DIM1, int DIM2, int DIM3, DATA_TYPE* INPLACE_ARRAY3 )
  • (   DATA_TYPE* INPLACE_FARRAY3, int DIM1, int DIM2, int DIM3 )
  • (   int DIM1, int DIM2, int DIM3, DATA_TYPE* INPLACE_FARRAY3 )

Четырехмерные:

  • (DATA_TYPE INPLACE_ARRAY4[ANY][ANY][ANY][ANY])
  • (DATA_TYPE* INPLACE_ARRAY4, DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4)
  • (DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, , DIM_TYPE DIM4, DATA_TYPE* INPLACE_ARRAY4)
  • (DATA_TYPE* INPLACE_FARRAY4, DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4)
  • (DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4, DATA_TYPE* INPLACE_FARRAY4)

Эти типмапы теперь проверяют, чтобы убедиться, что аргументы INPLACE_ARRAY используют родной порядок байтов. Если нет, генерируется исключение.

Также есть «плоский» массив на месте для ситуаций, когда вы хотите изменить или обработать каждый элемент независимо от количества измерений. Одним из примеров является функция «квантования», которая квантует каждый элемент массива на месте, будь то одномерный, двумерный или любой другой. Эта форма проверяет непрерывность, но позволяет использовать порядок C или Fortran.

N-мерный:

  • (DATA_TYPE* INPLACE_ARRAY_FLAT, DIM_TYPE DIM_FLAT)

Массивы Argout

Массивы Argout — это массивы, которые появляются в аргументах входных данных в C, но фактически являются выходными массивами. Этот паттерн часто встречается, когда существует более одной выходной переменной, и поэтому одного возвращаемого аргумента недостаточно. В Python обычный способ возврата нескольких аргументов — упаковать их в последовательность (кортеж, список и т. д.) и вернуть эту последовательность. Именно это делают типмапы argout. Если у обернутой функции, использующей эти типмапы argout, имеется более одного возвращаемого аргумента, они упаковываются в кортеж или список в зависимости от версии Python. Пользователь Python не передает эти массивы, они просто возвращаются. В случае, когда размерность задана, пользователь Python должен указать эту размерность в качестве аргумента. Подписи argout:

Одномерные:

  • (   DATA_TYPE ARGOUT_ARRAY1[ANY] )
  • (   DATA_TYPE* ARGOUT_ARRAY1, int DIM1 )
  • (   int DIM1, DATA_TYPE* ARGOUT_ARRAY1 )

Двумерные:

  • (   DATA_TYPE ARGOUT_ARRAY2[ANY][ANY] )

Трехмерные:

  • (   DATA_TYPE ARGOUT_ARRAY3[ANY][ANY][ANY] )

Четырехмерные:

  • (   DATA_TYPE ARGOUT_ARRAY4[ANY][ANY][ANY][ANY] )

Они обычно используются в ситуациях, когда в C/C++, вы бы выделяли массив(ы) в куче и вызывали функцию для заполнения значений массива(ов). В Python массивы выделяются за вас и возвращаются в виде новых объектов массива.

Обратите внимание, что мы поддерживаем DATA_TYPE* типмапы argout в одномерном случае, но не во двумерном или трехмерном. Это связано со спецификой синтаксиса типмапов SWIG и избежать этого нельзя. Обратите внимание, что для этих типов одномерных типмапов функция Python будет принимать один аргумент, представляющий DIM1.

Массивы Argout View

Массивы Argout View используются в тех случаях, когда ваш код C предоставляет вам представление своих внутренних данных и не требует выделения памяти пользователем. Это может быть опасно. Практически нет гарантий, что внутренние данные из кода C будут существовать в течение всего срока службы массива NumPy, который их оборачивает. Если пользователь уничтожит объект, предоставляющий представление данных, перед уничтожением массива NumPy, использование этого массива может привести к плохим обращениям к памяти или ошибкам сегментации. Тем не менее, в ситуациях работы с большими наборами данных у вас просто нет другого выбора.

Код C, подлежащий обертке для массивов argoutview, характеризуется указателями: указателями на размеры и двойными указателями на данные, чтобы эти значения могли быть переданы обратно пользователю. Поэтому подписи типмапов argoutview:

Одномерные:

  • ( DATA_TYPE** ARGOUTVIEW_ARRAY1, DIM_TYPE* DIM1 )
  • ( DIM_TYPE* DIM1, DATA_TYPE** ARGOUTVIEW_ARRAY1 )

Двумерные:

  • ( DATA_TYPE** ARGOUTVIEW_ARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2 )
  • ( DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEW_ARRAY2 )
  • ( DATA_TYPE** ARGOUTVIEW_FARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2 )
  • ( DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEW_FARRAY2 )

Трехмерные:

  • ( DATA_TYPE** ARGOUTVIEW_ARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)
  • ( DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEW_ARRAY3)
  • ( DATA_TYPE** ARGOUTVIEW_FARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)
  • ( DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEW_FARRAY3)

Четырехмерные:

  • (DATA_TYPE** ARGOUTVIEW_ARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEW_ARRAY4)
  • (DATA_TYPE** ARGOUTVIEW_FARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEW_FARRAY4)

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

Управляемые памятью массивы Argout View

Недавним дополнением к numpy.i являются типмапы, которые позволяют массивам argout с представлениями в управляемой памяти.

Одномерные:

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY1, DIM_TYPE* DIM1)
  • (DIM_TYPE* DIM1, DATA_TYPE** ARGOUTVIEWM_ARRAY1)

Двумерные:

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEWM_ARRAY2)
  • (DATA_TYPE** ARGOUTVIEWM_FARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEWM_FARRAY2)

Трехмерные:

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEWM_ARRAY3)
  • (DATA_TYPE** ARGOUTVIEWM_FARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEWM_FARRAY3)

Четырехмерные:

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEWM_ARRAY4)
  • (DATA_TYPE** ARGOUTVIEWM_FARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)
  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEWM_FARRAY4)

Выходные массивы

Файл интерфейса numpy.i не поддерживает типмапы для выходных массивов по нескольким причинам. Во-первых, аргументы возврата C/C++ ограничены одним значением. Это предотвращает получение информации о размерности общим способом. Во-вторых, массивы с жёстко заданной длиной не допускаются в качестве возвращаемых аргументов. Другими словами:

double[3] newVector(double x, double y, double z);

не является допустимым синтаксисом C/C++. Поэтому мы не можем предоставить типмапы вида:

%typemap(out) (TYPE[ANY]);

Если у вас возникнет ситуация, когда функция или метод возвращает указатель на массив, ваш лучший вариант — написать собственную версию обертываемой функции, либо с помощью %extend для методов классов, или с помощью %ignore и %rename для функций.

Другие распространённые типы: bool

Обратите внимание, что тип C++ bool не поддерживается в списке в разделе «Доступные типмапы». NumPy булевы значения — это один байт, в то время как C++ bool занимает четыре байта (по крайней мере, на моей системе). Поэтому:

%numpy_typemaps(bool, NPY_BOOL, int)

приведёт к типмапам, которые приведут к коду, ссылающемуся на неверные длины данных. Вы можете реализовать следующее расширение макроса:

%numpy_typemaps(bool, NPY_UINT, int)

для решения проблемы с длиной данных, и Входные массивы будут работать нормально, но Массивы на месте могут не пройти проверку типов.

Другие распространённые типы: complex

Преобразования типа для комплексных чисел с плавающей точкой также не поддерживаются автоматически. Это связано с тем, что Python и NumPy написаны на C, в котором нет встроенных комплексных типов. Как Python, так и NumPy реализуют свои (по существу эквивалентные) struct определения для комплексных переменных:

/* Python */
typedef struct {double real; double imag;} Py_complex;

/* NumPy */
typedef struct {float  real, imag;} npy_cfloat;
typedef struct {double real, imag;} npy_cdouble;

Мы могли бы реализовать:

%numpy_typemaps(Py_complex , NPY_CDOUBLE, int)
%numpy_typemaps(npy_cfloat , NPY_CFLOAT , int)
%numpy_typemaps(npy_cdouble, NPY_CDOUBLE, int)

что предоставило бы автоматические преобразования типов для массивов типа Py_complex, npy_cfloat и npy_cdouble. Однако, маловероятно, что существует какой-либо независимый (не Python, не NumPy) код приложения, который пользователи будут использовать SWIG для генерации интерфейса Python, также использующего эти определения для комплексных типов. Скорее всего, эти коды приложений будут определять свои собственные комплексные типы или, в случае C++, использовать std::complex. Предполагая, что эти структуры данных совместимы с комплексными типами Python и NumPy, %numpy_typemap расширения, как указано выше (с заменой первого аргумента типом комплексных данных пользователя), должны работать.

Массивы NumPy, скаляры и SWIG

SWIG имеет сложную систему проверки типов для числовых типов. Например, если ваша функция C/C++ ожидает целое число в качестве входного значения, сгенерированный SWIG код будет проверять как целые числа Python, так и длинные целые числа Python и поднимать ошибку переполнения, если предоставленное целое число Python слишком велико для преобразования к целому числу C. При внедрении массивов скаляров NumPy в ваш код Python, вы, возможно, извлечёте целое число из массива NumPy и попытаетесь передать его в обернутую SWIG функцию C/C++, которая ожидает int, но система проверки типов SWIG не распознает массив скаляров NumPy как целое число. (Часто это работает — зависит от того, распознает ли NumPy используемый вами тип целого числа как наследуемый от типа целого числа Python на вашей платформе. Иногда это означает, что код, работающий на 32-битной машине, потерпит неудачу на 64-битной машине.)

Если вы получите ошибку Python, похожую на следующую:

TypeError: in method 'MyClass_MyMethod', argument 2 of type 'int'

и аргумент, который вы передаёте, является целым числом, извлечённым из массива NumPy, то вы столкнулись с этой проблемой. Решение заключается в модификации системы преобразования типов SWIG для принятия скаляров массивов NumPy в дополнение к стандартным типам целых чисел. К счастью, эта возможность предоставлена. Просто скопируйте файл:

pyfragments.swg

в рабочую директорию сборки вашего проекта, и эта проблема будет решена. Рекомендуется сделать это в любом случае, так как это лишь увеличивает возможности вашего интерфейса Python.

Почему есть второй файл?

Система проверки и преобразования типов SWIG — это сложное сочетание макросов C, макросов SWIG, typemap SWIG и фрагментов SWIG. Фрагменты — способ условной вставки кода в ваш файл обёртки, если он нужен, и не вставка его, если он не нужен. Если несколько typemap требуют одного и того же фрагмента, фрагмент вставляется в ваш код обёртки только один раз.

Существует фрагмент для преобразования целого числа Python в C long. Есть другой фрагмент, который преобразует целое число Python в C int, вызывающий функцию, определённую в фрагменте long. Мы можем внести желаемые изменения, изменив определение фрагмента long. SWIG определяет активное определение фрагмента по принципу «первым пришёл, первым обслужен». То есть, нам нужно определить фрагмент для long преобразований до того, как SWIG сделает это внутренне. SWIG позволяет нам сделать это, поместив определения наших фрагментов в файл pyfragments.swg. Если бы мы поместили новые определения фрагментов в numpy.i, они были бы проигнорированы.

Вспомогательные функции

Файл numpy.i содержит несколько макросов и процедур, используемых им во внутренней работе для построения своих typemap. Однако эти функции могут быть полезны и в других частях вашего файла интерфейса. Эти макросы и процедуры реализованы как фрагменты, кратко описанные в предыдущем разделе. Если вы попытаетесь использовать один или несколько из следующих макросов или функций, но ваш компилятор сообщит, что не распознаёт символ, то вам нужно будет принудительно добавить эти фрагменты в ваш код, используя:

%fragment("NumPy_Fragments");

в вашем файле интерфейса SWIG.

Макросы

is_array(a)

Принимает значение true, если a не нулевое и может быть приведено к PyArrayObject*.

array_type(a)

Возвращает целочисленный код типа данных a, предполагая, что a может быть приведён к PyArrayObject*.

array_numdims(a)

Возвращает целое число, представляющее количество измерений a, предполагая, что a может быть приведён к PyArrayObject*.

array_dimensions(a)

Возвращает массив типа npy_intp длиной array_numdims(a), содержащий длины всех измерений a, предполагая, что a может быть приведён к PyArrayObject*.

array_size(a,i)

Возвращает размер i-го измерения a, предполагая, что a может быть приведён к PyArrayObject*.

array_strides(a)

Возвращает массив типа npy_intp длиной array_numdims(a), содержащий шаги всех измерений a, предполагая, что a может быть приведён к PyArrayObject*. Шаг - это расстояние в байтах между элементом и его непосредственным соседом вдоль одной оси.

array_stride(a,i)

Возвращает i-й шаг a, предполагая, что a может быть приведён к PyArrayObject*.

array_data(a)

Возвращает указатель типа void*, указывающий на буфер данных a, предполагая, что a может быть приведён к PyArrayObject*.

array_descr(a)

Возвращает ссылку на свойство dtype (PyArray_Descr*) a, предполагая, что a может быть приведён к PyArrayObject*.

array_flags(a)

Возвращает целое число, представляющее флаги a, предполагая, что a может быть приведён к PyArrayObject*.

array_enableflags(a,f)

Устанавливает флаг, представленный f для a, предполагая, что a может быть приведён к PyArrayObject*.

array_is_contiguous(a)

Принимает значение true, если a является непрерывным массивом. Эквивалентно (PyArray_ISCONTIGUOUS(a)).

array_is_native(a)

Принимает значение true, если буфер данных a использует родной порядок байтов. Эквивалентно (PyArray_ISNOTSWAPPED(a)).

array_is_fortran(a)

Принимает значение true, если a расположен в порядке Fortran.

Процедуры

pytype_string()

Возвращаемый тип: const char*

Аргументы:

  • PyObject* py_obj, общий объект Python.

Возвращает строку, описывающую тип py_obj.

typecode_string()

Возвращаемый тип: const char*

Аргументы:

  • int typecode, целочисленный код типа NumPy.

Возвращает строку, описывающую тип, соответствующий целочисленному коду NumPy typecode.

type_match()

Возвращаемый тип: int

Аргументы:

  • int actual_type, код типа массива NumPy.
  • int desired_type, желаемый код типа NumPy.

Убедитесь, что actual_type совместим с desired_type. Например, это позволяет соответствовать типам символов и байтов, или типам int и long. Сейчас это эквивалентно PyArray_EquivTypenums().

obj_to_array_no_conversion()

Возвращаемый тип: PyArrayObject*

Аргументы:

  • PyObject* input, общий объект Python.
  • int typecode, желаемый код типа NumPy.

Приводит input к PyArrayObject*, если это возможно, и гарантирует, что он имеет тип typecode. Если input нельзя привести, или typecode неверный, устанавливает ошибку Python и возвращает NULL.

obj_to_array_allow_conversion()

Возвращаемый тип: PyArrayObject*

Аргументы:

  • PyObject* input, общий объект Python.
  • int typecode, желаемый код типа NumPy для результирующего массива.
  • int* is_new_object, возвращает 0, если преобразование не выполнено, иначе 1.

Преобразует input в массив NumPy с заданным typecode. В случае успеха, возвращает действительный PyArrayObject* с правильным типом. В случае неудачи, строка ошибки Python будет установлена и функция возвращает NULL.

make_contiguous()

Возвращаемый тип: PyArrayObject*

Аргументы:

  • PyArrayObject* ary, массив NumPy.
  • int* is_new_object, возвращает 0, если преобразование не выполнено, иначе 1.
  • int min_dims, минимальное допустимое количество измерений.
  • int max_dims, максимальное допустимое количество измерений.

Проверяет, является ли ary непрерывным. Если да, возвращает указатель на входной массив и помечает его как не новый объект. Если нет, создаёт новый PyArrayObject* с исходными данными, помечает его как новый объект и возвращает указатель.

make_fortran()

Возвращаемый тип: PyArrayObject*

Аргументы:

  • PyArrayObject* ary, массив NumPy.
  • int* is_new_object, возвращает 0, если преобразование не выполнено, иначе 1.

Проверяет, является ли ary Fortran-непрерывным. Если да, возвращает указатель на входной массив и помечает его как не новый объект. Если нет, создаёт новый PyArrayObject* с исходными данными, помечает его как новый объект и возвращает указатель.

obj_to_array_contiguous_allow_conversion()

Возвращаемый тип: PyArrayObject*

Аргументы:

  • PyObject* input, общий объект Python.
  • int typecode, желаемый код типа NumPy для результирующего массива.
  • int* is_new_object, возвращает 0, если преобразование не выполнено, иначе 1.

Преобразует input в непрерывный PyArrayObject* заданного типа. Если входной объект не является непрерывным PyArrayObject*, будет создан новый.

obj_to_array_fortran_allow_conversion()

Возвращаемый тип: PyArrayObject*

Аргументы:

  • PyObject* input, общий объект Python.
  • int typecode, желаемый код типа NumPy для результирующего массива.
  • int* is_new_object, возвращает 0, если преобразование не выполнено, иначе 1.

Преобразует input в Fortran-непрерывный PyArrayObject* заданного типа. Если входной объект не является Fortran-непрерывным PyArrayObject*, будет создан новый.

require_contiguous()

Возвращаемый тип: int

Аргументы:

  • PyArrayObject* ary, массив NumPy.

Проверяет, является ли ary непрерывным. Если да, возвращает 1. Иначе, устанавливает ошибку Python и возвращает 0.

require_native()

Возвращаемый тип: int

Аргументы:

  • PyArray_Object* ary, массив NumPy.

Требует, чтобы ary не был переставлен байтами. Если массив не переставлен байтами, возвращает 1. Иначе, устанавливает ошибку Python и возвращает 0.

require_dimensions()

Возвращаемый тип: int

Аргументы:

  • PyArrayObject* ary, массив NumPy.
  • int exact_dimensions, желаемое количество измерений.

Требует, чтобы ary имел заданное количество измерений. Если массив имеет указанное количество измерений, возвращает 1. Иначе, устанавливает ошибку Python и возвращает 0.

require_dimensions_n()

Возвращаемый тип: int

Аргументы:

  • PyArrayObject* ary, массив NumPy.
  • int* exact_dimensions, массив целых чисел, представляющих допустимые количества измерений.
  • int n, длина exact_dimensions.

Требует, чтобы ary имел одно из заданных количеств измерений. Если массив имеет одно из указанных количеств измерений, возвращает 1. Иначе, устанавливает строку ошибки Python и возвращает 0.

require_size()

Возвращаемый тип: int

Аргументы:

  • PyArrayObject* ary, массив NumPy.
  • npy_int* size, массив, представляющий желаемые длины каждого измерения.
  • int n, длина size.

Требует, чтобы ary имел заданную форму. Если массив имеет заданную форму, возвращает 1. Иначе, устанавливает строку ошибки Python и возвращает 0.

require_fortran()

Возвращаемый тип: int

Аргументы:

  • PyArrayObject* ary, массив NumPy.

Требует, чтобы заданный PyArrayObject был Fortran-упорядоченным. Если PyArrayObject уже Fortran-упорядочен, ничего не делает. В противном случае устанавливает флаг Fortran-упорядоченности и пересчитывает шаги.

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

Существует множество ситуаций с C или C++ массивами/массивами NumPy, которые не покрываются простыми %include "numpy.i" и последующими %apply директивами.

Пример

Рассмотрим разумный прототип функции скалярного произведения:

double dot(int len, double* vec1, double* vec2);

Интерфейс Python, который мы хотим получить:

def dot(vec1, vec2):
    """
    dot(PyObject,PyObject) -> double
    """

Проблема здесь в том, что есть один аргумент размерности и два аргумента массива, а наши типмапы настроены для размерностей, которые применяются к одному массиву (фактически, SWIG не предоставляет механизм для сопоставления len с vec2, который принимает два аргумента Python). Рекомендуемое решение следующее:

%apply (int DIM1, double* IN_ARRAY1) {(int len1, double* vec1),
                                      (int len2, double* vec2)}
%rename (dot) my_dot;
%exception my_dot {
    $action
    if (PyErr_Occurred()) SWIG_fail;
}
%inline %{
double my_dot(int len1, double* vec1, int len2, double* vec2) {
    if (len1 != len2) {
        PyErr_Format(PyExc_ValueError,
                     "Arrays of lengths (%d,%d) given",
                     len1, len2);
        return 0.0;
    }
    return dot(len1, vec1, vec2);
}
%}

Если заголовочный файл, содержащий прототип для double dot(), также содержит другие прототипы, которые вы хотите обернуть, так что вам нужно %include этот заголовочный файл, то вам также понадобится директива %ignore dot;, размещенная после %rename и перед %include директивами. Или, если функция является методом класса, вам нужно будет использовать %extend вместо %inline в дополнение к %ignore.

Примечание об обработке ошибок: Обратите внимание, что my_dot возвращает double, но также может генерировать ошибку Python. Результирующая функция-обертка вернет представление числа с плавающей запятой Python 0.0, когда длины векторов не совпадают. Поскольку это не NULL, интерпретатор Python не будет знать, как проверить ошибку. По этой причине мы добавляем директиву %exception выше для my_dot, чтобы получить нужное поведение (обратите внимание, что $action — это макрос, который расширяется до действительного вызова my_dot). В общем случае, вы, вероятно, захотите написать макрос SWIG для выполнения этой задачи.

Другие ситуации

Существуют и другие ситуации обертки, в которых numpy.i может быть полезным, когда вы с ними столкнётесь.

  • В некоторых ситуациях вы можете использовать макрос %numpy_typemaps для реализации типмапов для ваших типов. Смотрите разделы Другие общие типы: bool или Другие общие типы: complex для примеров. Другая ситуация — если ваши размерности имеют тип, отличный от int (скажем, long, например):

    %numpy_typemaps(double, NPY_DOUBLE, long)
    
  • Вы можете использовать код в numpy.i для написания собственных типмапов. Например, если у вас был массив с пятью измерениями в качестве аргумента функции, вы могли бы скопировать и вставить соответствующие типмапы для четырёх измерений в ваш файл интерфейса. Изменения для четвёртого измерения были бы тривиальны.
  • Иногда лучший подход заключается в использовании директивы %extend для определения новых методов для ваших классов (или перегрузки существующих) которые принимают PyObject* (который либо является, либо может быть преобразован в PyArrayObject*) вместо указателя на буфер. В этом случае вспомогательные функции в numpy.i могут быть очень полезны.
  • Написание типмапов может быть немного неинтуитивным. Если у вас есть конкретные вопросы о написании типмапов SWIG для NumPy, разработчики numpy.i следят за почтовыми списками Numpy-discussion и Swig-user.

Заключительное замечание

Когда вы используете директиву %apply, как это обычно необходимо для использования numpy.i, она будет оставаться в силе, пока вы не сообщите SWIG, что этого не должно быть. Если аргументы функций или методов, которые вы обертываете, имеют общие имена, такие как length или vector, эти типмапы могут применяться в ситуациях, которых вы не ожидаете или не хотите. Поэтому всегда рекомендуется добавить директиву %clear после того, как вы закончили с конкретным типмапом:

%apply (double* IN_ARRAY1, int DIM1) {(double* vector, int length)}
%include "my_header.h"
%clear (double* vector, int length);

В общем случае, вы должны нацеливаться на эти подписи типмапов конкретно там, где вам нужно, а затем очищать их после завершения.

Резюме

Из коробки, numpy.i предоставляет типмапы, которые поддерживают преобразование между массивами NumPy и C-массивами:

  • Это может быть один из 12 разных скалярных типов: signed char, unsigned char, short, unsigned short, int, unsigned int, long, unsigned long, long long, unsigned long long, float и double.
  • Они поддерживают 74 различных подписи аргументов для каждого типа данных, включая:

    • Одномерные, двумерные, трехмерные и четырехмерные массивы.
    • Только вход, на месте, argout, argoutview и поведение памяти, управляемой argoutview.
    • Жестко заданные размерности, указание буфера данных, а затем размерностей, и указание размерностей, а затем буфера данных.
    • Поддержка как C-порядка («последнее измерение самое быстрое»), так и Fortran-порядка («первое измерение самое быстрое») для 2D, 3D и 4D массивов.

Файл интерфейса numpy.i также предоставляет дополнительные инструменты для разработчиков оберток, включая:

  • Макрос SWIG (%numpy_typemaps) с тремя аргументами для реализации 74 подписей аргументов для выбора пользователем (1) типа данных C, (2) типа данных NumPy (предполагается, что они совпадают) и (3) типа размерности.
  • Четырнадцать C-макросов и пятнадцать C-функций, которые можно использовать для написания специализированных типмапов, расширений или встроенных функций, обрабатывающих случаи, не охваченные предоставленными типмапами. Обратите внимание, что макросы и функции закодированы специально для работы с NumPy C/API независимо от номера версии NumPy, как до, так и после устаревания некоторых аспектов API после версии 1.6.

© 2005–2024 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/2.0/reference/swig.interface-file.html

Spec-Zone.ru

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