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

Введение#

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

double rms(double* seq, int n);

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

Если бы мы спроектировали rms, мы, вероятно, сделали её подпрограммой, принимающей входной массив длины n of 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, a SWIG файл интерфейса, который определяет серию типмапов, предназначенных для упрощения реализации типа преобразований, связанных с массивами, описанных выше. Например, предположим, что 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"

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

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

A 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 }

Типмапы из 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#

The 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().

Доступные типовые карты#

Директивы typemap, предоставляемые 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)

The 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 который должен быть одним из многих типов целых чисел.

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

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

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

1D:

  • (   DATA_TYPE IN_ARRAY1[ANY] )

  • (   DATA_TYPE* IN_ARRAY1, int DIM1 )

  • (   int DIM1, DATA_TYPE* IN_ARRAY1 )

2D:

  • (   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 )

3D:

  • (   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 )

4D:

  • (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 требуемого типа. Сигнатуры на месте — это

1D:

  • (   DATA_TYPE INPLACE_ARRAY1[ANY] )

  • (   DATA_TYPE* INPLACE_ARRAY1, int DIM1 )

  • (   int DIM1, DATA_TYPE* INPLACE_ARRAY1 )

2D:

  • (   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 )

3D:

  • (   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 )

4D:

  • (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 аргументы используют собственный порядок байтов. Если нет, возникает исключение.

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

ND:

  • (DATA_TYPE* INPLACE_ARRAY_FLAT, DIM_TYPE DIM_FLAT)

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

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

1D:

  • (   DATA_TYPE ARGOUT_ARRAY1[ANY] )

  • (   DATA_TYPE* ARGOUT_ARRAY1, int DIM1 )

  • (   int DIM1, DATA_TYPE* ARGOUT_ARRAY1 )

2D:

  • (   DATA_TYPE ARGOUT_ARRAY2[ANY][ANY] )

3D:

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

4D:

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

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

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

Массивы с выходными аргументами#

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

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

1D:

  • ( DATA_TYPE** ARGOUTVIEW_ARRAY1, DIM_TYPE* DIM1 )

  • ( DIM_TYPE* DIM1, DATA_TYPE** ARGOUTVIEW_ARRAY1 )

2D:

  • ( 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 )

3D:

  • ( 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)

4D:

  • (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 с управляемой памятью#

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

1D:

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY1, DIM_TYPE* DIM1)

  • (DIM_TYPE* DIM1, DATA_TYPE** ARGOUTVIEWM_ARRAY1)

2D:

  • (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)

3D:

  • (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)

4D:

  • (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)

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

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

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

Другие распространённые типы: комплексный#

Преобразования типов для комплексных типов с плавающей точкой также не поддерживаются автоматически. Это связано с тем, что 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.

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

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

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

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

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

%fragment("NumPy_Fragments");

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

Макросы#

is_array(a)

Вычисляется как истина, если a не являетсяNULL и может быть приведён к 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*. Шаг (stride) — это расстояние в байтах между элементом и его непосредственным соседом вдоль той же оси.

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 of a, предполагая a может быть приведён к PyArrayObject*.

array_is_contiguous(a)

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

array_is_native(a)

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

array_is_fortran(a)

Вычисляется как истина, если 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-код типа массива 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-непрерывным. Если да, вернуть входной указатель и пометить его как не новый объект. Если он не является 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 уже упорядочен по Фортрану, ничего не делать. Иначе установить флаг фортрановского порядка и пересчитать шаги.

Помимо предоставленных typemaps#

Существует множество ситуаций с массивами 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 float 0.0, когда длины векторов не совпадают. Поскольку это не NULL, интерпретатор Python не будет знать, что нужно проверить на ошибку. По этой причине мы добавляем %exception директива выше для my_dot чтобы получить желаемое поведение (обратите внимание, что $action является макросом, который раскрывается в допустимый вызов my_dot). В общем случае, вы, вероятно, захотите написать SWIG макрос для выполнения этой задачи.

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

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

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

    %numpy_typemaps(double, NPY_DOUBLE, long)
    
  • Вы можете использовать код в numpy.i чтобы написать свои собственные типомэпы. Например, если у вас есть пятимерный массив в качестве аргумента функции, вы можете скопировать соответствующие четырёхмерные типомэпы в ваш интерфейсный файл. Модификации для четвёртого измерения будут тривиальными.

  • Иногда лучший подход — использовать %extend директива для определения новых методов для ваших классов (или перегрузки существующих), которые принимают PyObject* (который либо является, либо может быть преобразован в PyArrayObject*) вместо указателя на буфер. В этом случае вспомогательные процедуры в numpy.i может быть очень полезным.

  • Написание типомапов может быть немного неинтуитивным. Если у вас есть конкретные вопросы о написании SWIG typemaps для NumPy, разработчики numpy.i следите за Numpy-discussion и Swig-user списки рассылки.

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

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

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

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

Сводка#

Из коробки, 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 массивов.

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

  • A SWIG макрос (%numpy_typemaps) с тремя аргументами для реализации 74 сигнатур аргументов для выбора пользователем (1) типа данных C, (2) типа данных NumPy (при условии их совпадения) и (3) типа размерности.

  • Четырнадцать C макросов и пятнадцать C функций, которые можно использовать для написания специализированных типмапов, расширений или встроенных функций, которые обрабатывают случаи, не охваченные предоставленными типмапами. Обратите внимание, что макросы и функции специально закодированы для работы с C/API NumPy независимо от номера версии NumPy, как до, так и после устаревания некоторых аспектов API после версии 1.6.