numpy.i: a SWIG Interface File for 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 мы проверяем корректность результата: что он не равен null и что он имеет одно измерение произвольной длины. После проверки этих состояний мы извлекаем буфер данных и длину в строках 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 и типов измерений %%%CODE_BLOCK_65%%:
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 в PyArrayObject* при возможности и убедитесь, что он имеет тип typecode. Если input нельзя преобразовать, или typecode неверен, установите ошибку Python и верните NULL.
obj_to_array_allow_conversion()
Тип возвращаемого значения: PyArrayObject*
Аргументы:
-
PyObject* input, общий объект Python. -
int typecode, желаемый целочисленный тип кода NumPy результата массива. -
int* is_new_object, возвращает значение 0, если преобразование не выполнено, иначе 1.
Преобразует input в массив NumPy с заданным typecode. При успехе возвращает действительный PyArrayObject* с правильным типом. При ошибке строка ошибки Python будет установлена, и функция вернёт NULL.
make_contiguous()
Тип возвращаемого значения: PyArrayObject*
Аргументы:
-
PyArrayObject* ary, массив NumPy. -
int* is_new_object, возвращает значение 0, если преобразование не выполнено, иначе 1. -
int min_dims, минимальное допустимое количество измерений. -
int max_dims, максимальное допустимое количество измерений.
Проверяет, является ли ary непрерывным. Если да, возвращает входной указатель и помечает его как не новый объект. Если нет, создаёт новый PyArrayObject* с исходными данными, помечает его как новый объект и возвращает указатель.
make_fortran()
Тип возвращаемого значения: PyArrayObject*
Аргументы
-
PyArrayObject* ary, массив NumPy. -
int* is_new_object, возвращает значение 0, если преобразование не выполнено, иначе 1.
Проверяет, является ли ary Fortran-непрерывным. Если да, возвращает входной указатель и помечает его как не новый объект. Если нет, создаёт новый PyArrayObject* с исходными данными, помечает его как новый объект и возвращает указатель.
obj_to_array_contiguous_allow_conversion()
Тип возвращаемого значения: PyArrayObject*
Аргументы:
-
PyObject* input, общий объект Python. -
int typecode, желаемый целочисленный тип кода NumPy результата массива. -
int* is_new_object, возвращает значение 0, если преобразование не выполнено, иначе 1.
Преобразует input в непрерывный PyArrayObject* указанного типа. Если входной объект не является непрерывным PyArrayObject*, будет создан новый.
obj_to_array_fortran_allow_conversion()
Тип возвращаемого значения: PyArrayObject*
Аргументы:
-
PyObject* input, общий объект Python. -
int typecode, желаемый целочисленный тип кода NumPy результата массива. -
int* is_new_object, возвращает значение 0, если преобразование не выполнено, иначе 1.
Преобразует input в Fortran-непрерывный PyArrayObject* указанного типа. Если входной объект не является Fortran-непрерывным PyArrayObject*, будет создан новый.
require_contiguous()
Тип возвращаемого значения: int
Аргументы:
-
PyArrayObject* ary, массив NumPy.
Проверяет, является ли ary непрерывным. Если да, возвращает 1. Иначе устанавливает ошибку Python и возвращает 0.
require_native()
Тип возвращаемого значения: int
Аргументы:
-
PyArray_Object* ary, массив NumPy.
Требует, чтобы ary не был байтовым. Если массив не байтовый, возвращает 1. Иначе устанавливает ошибку Python и возвращает 0.
require_dimensions()
Тип возвращаемого значения: int
Аргументы:
-
PyArrayObject* ary, массив NumPy. -
int exact_dimensions, желаемое число измерений.
Требует, чтобы у ary было заданное число измерений. Если число измерений совпадает, возвращает 1. Иначе устанавливает ошибку Python и возвращает 0.
require_dimensions_n()
Тип возвращаемого значения: int
Аргументы:
-
PyArrayObject* ary, массив NumPy. -
int* exact_dimensions, массив целых чисел, представляющих допустимые числа измерений. -
int n, длинаexact_dimensions.
Требует, чтобы у ary было одно из указанных чисел измерений. Если число измерений соответствует, возвращает 1. Иначе устанавливает строку ошибки Python и возвращает 0.
require_size()
Тип возвращаемого значения: int
Аргументы:
-
PyArrayObject* ary, массив NumPy. -
npy_int* size, массив, представляющий желаемые длины каждого измерения. -
int n, длинаsize.
Требует, чтобы у ary был указанный размер. Если размер совпадает, возвращает 1. Иначе устанавливает строку ошибки Python и возвращает 0.
require_fortran()
Тип возвращаемого значения: int
Аргументы:
-
PyArrayObject* ary, массив NumPy.
Требует, чтобы заданный PyArrayObject был в порядке Fortran. Если PyArrayObject уже в порядке Fortran, ничего не делает. Иначе устанавливает флаг порядка Fortran и пересчитывает шаги.
За пределами предоставленных 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 может быть немного неинтуитивным. Если у вас есть конкретные вопросы о написании SWIG typemap для 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–2020 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.19/reference/swig.interface-file.html