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, который должен быть одним из многих типов целых чисел.
Подписи typemaps в значительной степени различаются по имени указателя буфера. Имена с 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 целые числа и 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имеет порядок 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 и пересчитывает шаги.
За пределами предоставленных 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 может быть немного неинтуитивным. Если у вас есть конкретные вопросы о написании 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–2020 NumPy Developers
Licensed under the 3-clause BSD License.
https://numpy.org/doc/1.18/reference/swig.interface-file.html