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().
Доступные 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 предназначены для массивов с порядком Fortran, а имена с ARRAY — для массивов с порядком C (или 1D массивов).
Входные массивы
Входные массивы определяются как массивы данных, которые передаются в процедуру, но не изменяются на месте и не возвращаются пользователю. Массив 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
Массивы 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++. Поэтому мы не можем предоставить типомэпы вида:
%numpy_typemaps(bool, NPY_BOOL, int)
Если вы столкнулись со случаем, когда функция или метод возвращают указатель на массив, ваш лучший вариант — написать свою версию функции, которая должна быть обернута, либо с использованием %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 к 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 уже имеет порядок 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
"""
Проблема здесь в том, что есть один аргумент измерения и два аргумента массива, и наши typemap настроены для измерений, которые применяются к одному массиву (на самом деле, 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.
© 2005–2019 NumPy Developers
Licensed under the 3-clause BSD License.
https://docs.scipy.org/doc/numpy-1.14.5/reference/swig.interface-file.html