Numpy.i: файл интерфейса SWIG для NumPy
Введение
Простой генератор обёртки и интерфейса (или 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().
Доступные typemaps
Директивы typemap, предоставленные numpy.i для массивов разных типов данных, скажем, double и int, и размеров разных типов, скажем, int или long, идентичны друг другу, за исключением спецификаций C и NumPy типов. Поэтому typemaps реализуются (обычно за кулисами) с помощью макроса:
%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 для реализации typemaps для следующих C типов данных и int типов размеров:
signed charunsigned charshortunsigned shortintunsigned intlongunsigned longlong longunsigned long longfloatdouble
В последующих описаниях мы ссылаемся на обобщённый DATA_TYPE, который может быть любым из перечисленных выше C типов данных, и DIM_TYPE, который должен быть одним из многих типов целых чисел.
Сигнатуры typemap в значительной степени различаются по имени указателя буфера. Имена с FARRAY предназначены для массивов с фортран-порядком, а имена с 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)
Эти typemaps теперь проверяют, чтобы аргументы INPLACE_ARRAY использовали родной порядок байтов. Если нет, возникает исключение.
Также существует «плоский» массив на месте для ситуаций, когда вам нужно изменить или обработать каждый элемент независимо от числа измерений. Одним примером является функция «квантования», которая квантует каждый элемент массива на месте, будь то 1D, 2D или любой другой. Эта форма проверяет непрерывность, но допускает порядок C или Fortran.
ND:
(DATA_TYPE* INPLACE_ARRAY_FLAT, DIM_TYPE DIM_FLAT)
Массивы Argout
Массивы Argout — это массивы, которые появляются в входных аргументах в C, но фактически являются выходными массивами. Этот шаблон часто встречается, когда существует более одной выходной переменной, и одного возвращаемого аргумента недостаточно. В Python общепринятый способ возвращения нескольких аргументов заключается в их упаковке в последовательность (кортеж, список и т. д.) и возвращении этой последовательности. Именно это делают типмапы argout. Если обернутая функция, использующая эти типмапы argout, имеет более одного возвращаемого аргумента, они упаковываются в кортеж или список в зависимости от версии 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 в 1D, но не в 2D или 3D. Это из-за особенности синтаксиса типмапов SWIG и избежать этого невозможно. Обратите внимание, что для этих типов 1D типмапов функция Python будет принимать один аргумент, представляющий DIM1.
Массивы Argout View
Массивы 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 View
Недавнее дополнение к numpy.i — это типмапы, которые разрешают массивы argout с представлениями в управляемой памяти. См. обсуждение здесь.
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)
Выходные массивы
Файл интерфейса 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, типмапов SWIG и фрагментов SWIG. Фрагменты — это способ условной вставки кода в ваш файл обертки, если он нужен, и не вставка его, если нет. Если несколько типмапов требуют одного и того же фрагмента, фрагмент вставляется в ваш код обёртки только один раз.
Существует фрагмент для преобразования целого числа Python в C long. Есть другой фрагмент, который преобразует целое число Python в C int, вызывающий функцию, определённую в фрагменте long. Мы можем внести необходимые изменения, изменив определение фрагмента long. SWIG определяет активное определение фрагмента, используя систему «первый пришёл, первый обслужен». То есть, нам нужно определить фрагмент для преобразований long перед тем, как SWIG сделает это внутренне. SWIG позволяет нам сделать это, поместив наши определения фрагментов в файл pyfragments.swg. Если мы поместили новые определения фрагментов в numpy.i, они будут проигнорированы.
Вспомогательные функции
Файл 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*. Шаг — это расстояние в байтах между элементом и его непосредственным соседом вдоль одной оси. - 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)
- Оценивается как истинное, если
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 в массив NumPy, если это допустимо, и убедитесь, что он имеет тип 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 в массив NumPy с порядком Fortran указанного типа. Если входной объект не является массивом NumPy с порядком Fortran, будет создан новый.
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 и пересчитайте шаги.
За пределами предоставленных 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
"""
Проблема в том, что здесь один аргумент измерения и два аргумента массива, а наши typemaps настроены для измерений, которые применяются к одному массиву (на самом деле, 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для реализации typemap для собственных типов. См. разделы Другие распространённые типы: bool или Другие распространённые типы: complex для примеров. Другая ситуация — если ваши размеры имеют тип, отличный отint(скажем,long, например):%numpy_typemaps(double, NPY_DOUBLE, long)
- Вы можете использовать код в
numpy.iдля написания собственных typemap. Например, если у вас есть пятимерный массив в качестве аргумента функции, вы можете скопировать и вставить соответствующие четырёхмерные typemap в ваш интерфейсный файл. Модификации для четвёртого измерения будут тривиальными. - Иногда лучше всего использовать директиву
%extendдля определения новых методов для ваших классов (или перегрузки существующих), которые принимаютPyObject*(который либо является, либо может быть преобразован вPyArrayObject*) вместо указателя на буфер. В этом случае вспомогательные функции вnumpy.iмогут быть очень полезны. - Написание typemap может быть немного неинтуитивным. Если у вас есть конкретные вопросы о написании typemap SWIG для NumPy, разработчики
numpy.iотслеживают почтовые списки Numpy-discussion и Swig-user.
Заключение
При использовании директивы %apply, как обычно необходимо для использования numpy.i, она будет оставаться в силе, пока вы не сообщите SWIG, что её не должно быть. Если аргументы функций или методов, которые вы обёртываете, имеют общие имена, такие как length или vector, эти typemap могут применяться в ситуациях, которых вы не ожидаете или не хотите. Поэтому всегда рекомендуется добавить директиву %clear после завершения работы со специфическими typemap:
%apply (double* IN_ARRAY1, int DIM1) {(double* vector, int length)}
%include "my_header.h"
%clear (double* vector, int length);
В общем случае вы должны нацеливать эти подписи typemap конкретно там, где вам нужно, а затем очищать их после завершения работы.
Резюме
Встроенно numpy.i предоставляет typemap, которые поддерживают преобразование между массивами 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-функций, которые могут быть использованы для написания специализированных typemap, расширений или встроенных функций, которые обрабатывают случаи, не покрытые предоставленными typemap. Обратите внимание, что макросы и функции закодированы специально для работы с NumPy C/API независимо от номера версии NumPy, как до, так и после устаревания некоторых аспектов API после версии 1.6.
© 2008–2016 NumPy Developers
Licensed under the NumPy License.
https://docs.scipy.org/doc/numpy-1.11.0/reference/swig.interface-file.html