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 (или одномерных массивов).
Входные массивы
Входные массивы определяются как массивы данных, которые передаются в процедуру, но не изменяются на месте и не возвращаются пользователю. Таким образом, входной массив 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, так и целые числа типа long 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 содержит несколько макросов и процедур, которые он использует во внутреннем процессе построения своих typemap. Однако, эти функции могут быть полезны и в других частях вашего интерфейсного файла. Эти макросы и процедуры реализованы как фрагменты, краткое описание которых приведено в предыдущем разделе. Если вы попытаетесь использовать один или несколько из следующих макросов или функций, но ваш компилятор сообщит о неизвестном символе, то вам необходимо добавить эти фрагменты в ваш код:
%fragment("NumPy_Fragments");
в вашем интерфейсном файле SWIG.
Макросы
- is_array(a)
- Возвращает значение true, если
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)
- Возвращает true, если
aявляется непрерывным массивом. Эквивалентно(PyArray_ISCONTIGUOUS(a)). - array_is_native(a)
- Возвращает true, если буфер данных
aиспользует родной порядок байтов. Эквивалентно(PyArray_ISNOTSWAPPED(a)). - array_is_fortran(a)
- Возвращает true, если
aупорядочен по Фортрану.
Процедуры
pytype_string()
Тип возвращаемого значения: const char*
Аргументы:
-
PyObject* py_obj, общий объект Python.
Возвращает строку, описывающую тип py_obj.
typecode_string()
Тип возвращаемого значения: const char*
Аргументы:
-
int typecode, целочисленный тип данных NumPy.
Возвращает строку, описывающую тип, соответствующий целочисленному типу NumPy typecode.
type_match()
Тип возвращаемого значения: int
Аргументы:
-
int actual_type, тип NumPy массива. -
int desired_type, желаемый тип NumPy.
Убедитесь, что actual_type совместим с desired_type. Например, это позволяет совпадение типов символов и байтов, или типов int и long. Это сейчас эквивалентно PyArray_EquivTypenums().
obj_to_array_no_conversion()
Тип возвращаемого значения: PyArrayObject*
Аргументы:
-
PyObject* input, общий объект Python. -
int typecode, желаемый целочисленный тип NumPy.
Преобразует input в массив PyArrayObject* , если это возможно, и убедитесь, что он имеет тип typecode. Если input не может быть преобразован или тип typecode неверный, возникает ошибка Python, и возвращается NULL.
obj_to_array_allow_conversion()
Тип возвращаемого значения: PyArrayObject*
Аргументы:
-
PyObject* input, общий объект Python. -
int typecode, желаемый целочисленный тип NumPy результирующего массива. -
int* is_new_object, возвращает 0, если преобразование не выполнено, иначе 1.
Преобразует input в массив NumPy с заданным typecode. В случае успеха возвращает допустимый массив PyArrayObject* с правильным типом. В случае неудачи устанавливается строка ошибки Python, и процедура возвращает NULL.
make_contiguous()
Тип возвращаемого значения: PyArrayObject*
Аргументы:
-
PyArrayObject* ary, массив NumPy. -
int* is_new_object, возвращает 0, если преобразование не выполнено, иначе 1. -
int min_dims, минимальное допустимое число измерений. -
int max_dims, максимальное допустимое число измерений.
Проверяет, является ли ary непрерывным. Если да, возвращает входной указатель и помечает его как не новый объект. Если нет, создаёт новый массив PyArrayObject* с исходными данными, помечает его как новый объект и возвращает указатель.
make_fortran()
Тип возвращаемого значения: PyArrayObject*
Аргументы
-
PyArrayObject* ary, массив NumPy. -
int* is_new_object, возвращает 0, если преобразование не выполнено, иначе 1.
Проверяет, является ли ary массивом Фортрана. Если да, возвращает входной указатель и помечает его как не новый объект. Если нет, создаёт новый массив 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 в массив Фортрана PyArrayObject* указанного типа. Если входной объект не является массивом Фортрана 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 был упорядочен по Фортрану. Если PyArrayObject уже упорядочен по Фортрану, ничего не делает. В противном случае устанавливает флаг упорядоченности по Фортрану и пересчитывает шаги.
За пределами предоставленных typemap
Существуют многочисленные ситуации с массивами 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 может быть немного неинтуитивным. Если у вас есть конкретные вопросы о написании 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.
© 2008–2016 NumPy Developers
Licensed under the NumPy License.
https://docs.scipy.org/doc/numpy-1.10.1/reference/swig.interface-file.html