numpy.i:NumPy 的 SWIG 接口文件#

简介#

简单封装器和接口生成器(Simple Wrapper and Interface Generator,或 SWIG )是一个强大的工具,用于生成封装代码,以便与各种脚本语言进行接口. SWIG 可以解析头文件,并且仅使用代码原型,即可创建与目标语言的接口. 但是 SWIG 并非万能. 例如,它无法从以下原型中得知:

double rms(double* seq, int n);

seq 到底是什么.它是一个要进行就地修改的单个值吗?它是一个数组吗?如果是,它的长度是多少?它是只进输入?只出输出?还是输入输出? SWIG 无法确定这些细节,也没有尝试这样做.

如果我们设计了 rms ,我们可能会将其设计为这样一个例程:它接受一个长度为 ndouble 类型的只读输入数组 seq ,并返回均方根.然而, SWIG 的默认行为是创建一个可以编译的包装函数,但几乎不可能以 C 例程的预期方式从脚本语言中使用.

对于 Python,处理连续(或技术上来说,是跨步的)同构数据块的首选方式是使用 NumPy,它提供了对多维数据数组的完整面向对象访问.因此, rms 函数最符合逻辑的 Python 接口应该是(包括文档字符串):

def rms(seq):
    """
    rms: return the root mean square of a sequence
    rms(numpy.ndarray) -> double
    rms(list) -> double
    rms(tuple) -> double
    """

其中 seq 将是一个 double 值的 NumPy 数组,它的长度 n 将在内部从 seq 中提取,然后传递给 C 例程.更好的是,由于 NumPy 支持从任意 Python 序列构造数组,因此 seq 本身可以是一个几乎任意的序列(只要每个元素都可以转换为 double ),并且包装器代码将在内部将其转换为 NumPy 数组,然后再提取其数据和长度.

SWIG 允许通过一种称为类型映射的机制来定义这些类型的转换.本文档提供了有关如何使用 numpy.i 的信息, numpy.i 是一个 SWIG 接口文件,它定义了一系列类型映射,旨在使实现上述与数组相关的转换类型相对简单.例如,假设上面定义的 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"

类型映射根据一个或多个函数参数的列表进行键控,可以通过类型或类型和名称进行键控.我们将此类列表称为签名. numpy.i 定义的许多类型映射之一在上面使用,其签名是 (double IN_ARRAY1, int DIM1) .参数名称旨在表明 double* 参数是单维输入数组,而 int 表示该维度的大小.这正是 rms 原型中的模式.

最有可能的是,要包装的任何实际原型都不会具有参数名称 IN_ARRAY1DIM1 .我们使用 SWIG %apply 指令将类型为 double 的一维输入数组的类型映射应用于 rms 使用的实际原型.因此,有效地使用 numpy.i 需要知道哪些类型映射可用以及它们的作用.

包含上面给出的 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 }

来自 numpy.i 的类型映射负责以下代码行:12–20,25 和 30.第 10 行解析了 rms 函数的输入.从格式字符串 "O:rms" 中,我们可以看到参数列表预计是一个 Python 对象(由冒号前的 O 指定),其指针存储在 obj0 中.调用了许多由 numpy.i 提供的函数,以进行和检查从通用 Python 对象到 NumPy 数组的(可能)转换.这些函数在 Helper Functions 部分中进行了解释,但希望它们的名称是不言自明的.在第 12 行,我们使用 obj0 构造一个 NumPy 数组.在第 17 行,我们检查结果的有效性:它是否为非空以及它是否具有任意长度的单个维度.一旦验证了这些状态,我们就在第 19 行和第 20 行提取数据缓冲区和长度,以便我们可以在第 22 行调用底层 C 函数.第 25 行为我们创建了一个不再需要的新数组的情况执行内存管理.

这段代码有大量的错误处理.请注意, SWIG_fail 是一个宏,代表 goto fail ,指向第 28 行的标签.如果用户提供的参数数量错误,这将在第 10 行被捕获.如果 NumPy 数组的构造失败或生成了维度数量错误的数组,这些错误将在第 17 行被捕获.最后,如果检测到错误,内存仍然会在第 30 行被正确管理.

请注意,如果 C 函数签名是不同的顺序:

double rms(int n, double* seq);

那么 SWIG 将无法将上面给出的类型映射签名与 rms 的参数列表匹配.幸运的是, numpy.i 拥有一组类型映射,其中数据指针放在最后:

%apply (int DIM1, double* IN_ARRAY1) {(int n, double* seq)};

这仅仅具有切换生成的代码中第 3 行和第 4 行的 arg1arg2 定义,以及在第 19 行和第 20 行中的赋值的效果.

使用 numpy.i#

numpy.i 文件目前位于 numpy 安装目录下的 tools/swig 子目录中.通常,你希望将其复制到你正在开发包装器的目录中.

一个仅使用单个 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() .

可用的类型映射#

numpy.i 提供的用于不同数据类型(例如 doubleint )数组以及不同类型(例如 intlong )维度的类型映射指令,除了 C 和 NumPy 类型规范外,彼此相同. 因此,类型映射通过宏(通常在幕后)来实现:

%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 宏来实现以下 C 数据类型和 int 维度类型的类型映射:

  • signed char

  • unsigned char

  • short

  • unsigned short

  • int

  • unsigned int

  • long

  • unsigned long

  • long long

  • unsigned long long

  • float

  • double

在以下描述中,我们引用一个通用的 DATA_TYPE ,它可以是上面列出的任何 C 数据类型,而 DIM_TYPE 应该是多种整数类型之一.

类型映射签名主要通过赋予缓冲区指针的名称来区分. 带有 FARRAY 的名称用于 Fortran 顺序数组,而带有 ARRAY 的名称用于 C 顺序(或 1D 数组).

输入数组#

输入数组定义为传递到例程中但不会就地更改或返回给用户的数据数组. 因此,Python 输入数组可以几乎是任何可以转换为请求的数组类型的 Python 序列(例如列表). 输入数组的签名是

1维:

  • (   DATA_TYPE IN_ARRAY1[ANY] )

  • (   DATA_TYPE IN_ARRAY1, int DIM1 )

  • (   int DIM1, DATA_TYPE IN_ARRAY1 )

2维:

  • (   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 )

3维:

  • (   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 )

4维:

  • (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数组.原地签名是

1维:

  • (   DATA_TYPE INPLACE_ARRAY1[ANY] )

  • (   DATA_TYPE INPLACE_ARRAY1, int DIM1 )

  • (   int DIM1, DATA_TYPE INPLACE_ARRAY1 )

2维:

  • (   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 )

3维:

  • (   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 )

4维:

  • (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)

输出参数数组#

输出参数数组是在 C 的输入参数中出现,但实际上是输出数组的数组.当有多个输出变量且单个返回参数不足时,通常会出现这种情况.在 Python 中,返回多个参数的常规方法是将它们打包成一个序列(元组,列表等)并返回该序列.这就是 argout 类型映射所做的事情.如果一个使用这些 argout 类型映射的包装函数有多个返回参数,它们会被打包成一个元组或列表,具体取决于 Python 的版本.Python 用户不会传入这些数组,它们只是被返回.对于指定维度的情况,Python 用户必须将该维度作为参数提供.argout 的签名是

1维:

  • (   DATA_TYPE ARGOUT_ARRAY1[ANY] )

  • (   DATA_TYPE ARGOUT_ARRAY1, int DIM1 )

  • (   int DIM1, DATA_TYPE ARGOUT_ARRAY1 )

2维:

  • (   DATA_TYPE ARGOUT_ARRAY2[ANY][ANY] )

3维:

  • (   DATA_TYPE ARGOUT_ARRAY3[ANY][ANY][ANY] )

4维:

  • (   DATA_TYPE ARGOUT_ARRAY4[ANY][ANY][ANY][ANY] )

这些通常用于在 C/C++ 中,您将在堆上分配一个或多个数组,并调用该函数来填充数组值的情况.在 Python 中,这些数组会为您分配,并作为新的数组对象返回.

请注意,我们支持 1D 中的 DATA_TYPE* argout 类型映射,但不支持 2D 或 3D.这是由于 SWIG 类型映射语法的怪癖造成的,无法避免.请注意,对于这些类型的 1D 类型映射,Python 函数将接受表示 DIM1 的单个参数.

输出参数视图数组#

当您的 C 代码为您提供其内部数据的视图,并且不需要用户分配任何内存时,可以使用 Argoutview 数组.这可能很危险.几乎无法保证来自 C 代码的内部数据在封装它的 NumPy 数组的整个生命周期内保持存在.如果用户在销毁 NumPy 数组之前销毁了提供数据视图的对象,则使用该数组可能会导致错误的内存引用或段错误.然而,在处理大型数据集的情况下,您别无选择.

要为 argoutview 数组包装的 C 代码的特点是指针:指向维度的指针和指向数据的双指针,以便可以将这些值传递回用户.因此,argoutview 类型映射的签名是

1维:

  • ( DATA_TYPE ARGOUTVIEW_ARRAY1, DIM_TYPE DIM1 )

  • ( DIM_TYPE DIM1, DATA_TYPE ARGOUTVIEW_ARRAY1 )

2维:

  • ( 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 )

3维:

  • ( 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)

4维:

  • (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 视图数组#

numpy.i 最近新增了类型映射,允许使用指向托管内存的视图作为 argout 数组.

1维:

  • (DATA_TYPE ARGOUTVIEWM_ARRAY1, DIM_TYPE DIM1)

  • (DIM_TYPE DIM1, DATA_TYPE ARGOUTVIEWM_ARRAY1)

2维:

  • (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)

3维:

  • (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)

4维:

  • (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#

请注意, Available Typemaps 部分的列表中不支持 C++ 类型 bool .NumPy 布尔值是单字节,而 C++ bool 是四字节(至少在我的系统上是这样).因此:

%numpy_typemaps(bool, NPY_BOOL, int)

将导致类型映射生成引用不正确数据长度的代码.你可以实现以下宏扩展:

%numpy_typemaps(bool, NPY_UINT, int)

来修正数据长度问题, Input Arrays 将可以正常工作,但 In-Place Arrays 可能会失败类型检查.

其他常用类型:complex#

复数浮点类型的类型映射转换也不会自动支持. 这是因为 Python 和 NumPy 是用 C 编写的,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_cfloatnpy_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 -封装的期望 int 的 C/C++ 函数,但 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 使用“先到先得”系统来确定片段的主动定义.也就是说,我们需要在 SWIG 内部定义之前,定义 long 转换的片段. SWIG 允许我们通过将我们的片段定义放在 pyfragments.swg 文件中来实现这一点.如果我们把新的片段定义放在 numpy.i 中,它们将被忽略.

辅助函数#

numpy.i 文件包含几个宏和例程,它在内部使用它们来构建其类型映射.但是,这些函数在您的界面文件中可能在其他地方很有用.这些宏和例程被实现为片段,这在上一节中简要描述过.如果您尝试使用以下宏或函数中的一个或多个,但您的编译器抱怨它无法识别该符号,那么您需要强制这些片段使用:

%fragment("NumPy_Fragments");

在你的 SWIG 接口文件中.

#

is_array(a)

如果 aNULL 并且可以强制转换为 PyArrayObject* ,则求值为真.

array_type(a)

假设 a 可以强制转换为 PyArrayObject* ,则求值为 a 的整数数据类型代码.

array_numdims(a)

假设 a 可以强制转换为 PyArrayObject* ,则求值为 a 的整数维度数.

array_dimensions(a)

假设 a 可以强制转换为 PyArrayObject* ,则求值为类型为 npy_intp 且长度为 array_numdims(a) 的数组,给出 a 的所有维度的长度.

array_size(a,i)

假设 a 可以强制转换为 PyArrayObject* ,则求值为 a 的第 i 维的大小.

array_strides(a)

假设 a 可以强制转换为 PyArrayObject* ,则求值为类型为 npy_intp 且长度为 array_numdims(a) 的数组,给出 a 的所有维度的步长.步长是一个元素与其沿同一轴的直接邻居之间的字节距离.

array_stride(a,i)

假设 a 可以强制转换为 PyArrayObject* ,则求值为 a 的第 i 个步长.

array_data(a)

假设 a 可以强制转换为 PyArrayObject* ,则求值为指向 a 的数据缓冲区的 void* 类型的指针.

array_descr(a)

返回对 a 的 dtype 属性 ( PyArray_Descr* ) 的借用引用,假设 a 可以强制转换为 PyArrayObject* .

array_flags(a)

返回一个整数,表示 a 的标志,假设 a 可以强制转换为 PyArrayObject* .

array_enableflags(a,f)

设置 a 的由 f 表示的标志,假设 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_typedesired_type 兼容.例如,这允许字符和字节类型或 int 和 long 类型匹配.这现在等同于 PyArray_EquivTypenums() .

obj_to_array_no_conversion()

返回类型: PyArrayObject*

参数:

  • PyObject input ,一个通用的 Python 对象.

  • int typecode ,所需的 NumPy 类型代码.

如果合法,将 input 强制转换为 PyArrayObject* ,并确保它是 typecode 类型.如果无法转换 inputtypecode 错误,则设置 Python 错误并返回 NULL .

obj_to_array_allow_conversion()

返回类型: PyArrayObject*

参数:

  • PyObject input ,一个通用的 Python 对象.

  • int typecode ,结果数组所需的 NumPy 类型代码.

  • int is_new_object ,如果没有执行转换,则返回值 0,否则返回 1.

使用给定的 typecodeinput 转换为 NumPy 数组.成功时,返回具有正确类型的有效 PyArrayObject* .失败时,将设置 Python 错误字符串,并且该例程返回 NULL .

make_contiguous()

返回类型: PyArrayObject*

参数:

  • PyArrayObject ary ,一个 NumPy 数组.

  • int is_new_object ,如果没有执行转换,则返回值 0,否则返回 1.

  • int min_dims ,最小允许维度.

  • int max_dims ,最大允许维度.

检查 ary 是否是连续的.如果是,则返回输入指针并将其标记为不是新对象.如果它不是连续的,则使用原始数据创建一个新的 PyArrayObject* ,将其标记为新对象并返回指针.

make_fortran()

返回类型: PyArrayObject*

参数

  • PyArrayObject ary ,一个 NumPy 数组.

  • int is_new_object ,如果没有执行转换,则返回值 0,否则返回 1.

检查 ary 是否是 Fortran 连续的.如果是,则返回输入指针并将其标记为不是新对象.如果它不是 Fortran 连续的,则使用原始数据创建一个新的 PyArrayObject* ,将其标记为新对象并返回指针.

obj_to_array_contiguous_allow_conversion()

返回类型: PyArrayObject*

参数:

  • PyObject input ,一个通用的 Python 对象.

  • int typecode ,结果数组所需的 NumPy 类型代码.

  • int is_new_object ,如果没有执行转换,则返回值 0,否则返回 1.

input 转换为指定类型的连续 PyArrayObject* .如果输入对象不是连续的 PyArrayObject* ,则将创建一个新的对象,并且将设置新对象标志.

obj_to_array_fortran_allow_conversion()

返回类型: PyArrayObject*

参数:

  • PyObject input ,一个通用的 Python 对象.

  • int typecode ,结果数组所需的 NumPy 类型代码.

  • int is_new_object ,如果没有执行转换,则返回值 0,否则返回 1.

input 转换为指定类型的 Fortran 连续 PyArrayObject* .如果输入对象不是 Fortran 连续的 PyArrayObject* ,则将创建一个新的对象,并且将设置新对象标志.

require_contiguous()

返回类型: int

参数:

  • PyArrayObject ary ,一个 NumPy 数组.

测试 ary 是否是连续的.如果是,则返回 1.否则,设置 Python 错误并返回 0.

require_native()

返回类型: int

参数:

  • PyArray_Object ary ,一个 NumPy 数组.

要求 ary 不是字节交换的.如果数组不是字节交换的,则返回 1.否则,设置 Python 错误并返回 0.

require_dimensions()

返回类型: int

参数:

  • PyArrayObject ary ,一个 NumPy 数组.

  • int exact_dimensions ,所需的维度数.

要求 ary 具有指定的维度数.如果数组具有指定的维度数,则返回 1.否则,设置 Python 错误并返回 0.

require_dimensions_n()

返回类型: int

参数:

  • PyArrayObject ary ,一个 NumPy 数组.

  • int exact_dimensions ,一个整数数组,表示可接受的维度数.

  • int n , exact_dimensions 的长度.

要求 ary 具有指定维度数列表中的一个.如果数组具有指定的维度数之一,则返回 1.否则,设置 Python 错误字符串并返回 0.

require_size()

返回类型: int

参数:

  • PyArrayObject ary ,一个 NumPy 数组.

  • npy_int size ,一个表示每个维度的所需长度的数组.

  • int n , size 的长度.

要求 ary 具有指定的形状.如果数组具有指定的形状,则返回 1.否则,设置 Python 错误字符串并返回 0.

require_fortran()

返回类型: int

参数:

  • PyArrayObject ary ,一个 NumPy 数组.

要求给定的 PyArrayObject 是 Fortran 顺序的.如果 PyArrayObject 已经是 Fortran 顺序的,则不执行任何操作.否则,设置 Fortran 排序标志并重新计算步幅.

超出提供的类型映射#

有很多 C 或 C++ 数组/NumPy 数组的情况不能通过简单的 %include "numpy.i" 和后续的 %apply 指令来覆盖.

一个常见的例子#

考虑一个合理的点积函数原型:

double dot(int len, double* vec1, double* vec2);

我们想要的 Python 接口是:

def dot(vec1, vec2):
    """
    dot(PyObject,PyObject) -> double
    """

这里的问题是有一个维度参数和两个数组参数,我们的类型映射是为应用于单个数组的维度设置的(实际上, SWIG 没有提供将 lenvec2 关联的机制,它接受两个 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 错误.当向量长度不匹配时,生成的包装函数将返回 0.0 的 Python 浮点数表示形式.由于这不是 NULL ,Python 解释器将不知道检查错误.因此,我们在上面为 my_dot 添加了 %exception 指令以获得我们想要的行为(请注意, $action 是一个宏,它扩展为对 my_dot 的有效调用).一般来说,您可能需要编写一个 SWIG 宏来执行此任务.

其他情况#

在其他包装情况下,当您遇到它们时, numpy.i 可能会有所帮助.

  • 在某些情况下,您可以使用 %numpy_typemaps 宏来实现您自己类型的类型映射.有关示例,请参见 Other Common Types: boolOther Common Types: complex 部分.另一种情况是,如果您的维度是 int 以外的类型(例如 long ):

    %numpy_typemaps(double, NPY_DOUBLE, long)
    
  • 您可以使用 numpy.i 中的代码来编写自己的类型映射.例如,如果您的函数参数是一个五维数组,您可以将相应的四维类型映射剪切并粘贴到您的接口文件中. 对第四维的修改将是微不足道的.

  • 有时,最好的方法是使用 %extend 指令为您的类定义新方法(或重载现有方法),该方法接受一个 PyObject* (它可以是或可以转换为 PyArrayObject* )而不是指向缓冲区的指针.在这种情况下, numpy.i 中的辅助例程非常有用.

  • 编写类型映射可能有点不直观.如果您对编写 NumPy 的 SWIG 类型映射有具体问题, numpy.i 的开发人员会监控 Numpy-discussionSwig-user 邮件列表.

最后的说明#

当您使用 %apply 指令(通常使用 numpy.i 是必要的)时,它将一直有效,直到您告诉 SWIG 它不应该这样做为止. 如果您要包装的函数或方法的参数具有通用名称,例如 lengthvector ,则这些类型映射可能会应用于您不期望或不希望的情况. 因此,在完成特定的类型映射后,始终建议添加 %clear 指令:

%apply (double* IN_ARRAY1, int DIM1) {(double* vector, int length)}
%include "my_header.h"
%clear (double* vector, int length);

通常,您应该在需要的地方专门针对这些类型映射签名,然后在完成后清除它们.

总结#

开箱即用, numpy.i 提供了类型映射,支持 NumPy 数组和 C 数组之间的转换:

  • 可以是 12 种不同的标量类型之一: signed char , unsigned char , short , unsigned short , int , unsigned int , long , unsigned long , long long , unsigned long long , floatdouble .

  • 它支持每种数据类型 74 种不同的参数签名,包括:

    • 一维,二维,三维和四维数组.

    • 只输入,原地操作,argout,argoutview 和内存管理的 argoutview 行为.

    • 硬编码的维度,数据缓冲区后接维度规范,以及维度后接数据缓冲区规范.

    • 对于 2D,3D 和 4D 数组,同时支持 C 顺序(“最后一维最快”)或 Fortran 顺序(“第一维最快”).

numpy.i 接口文件还为包装器开发者提供了其他工具,包括:

  • 一个 SWIG 宏 ( %numpy_typemaps ),带有三个参数,用于实现用户选择的(1)C 数据类型,(2)NumPy 数据类型(假设它们匹配),以及(3)维度类型的 74 个参数签名.

  • 十四个 C 宏和十五个 C 函数,可用于编写专门的类型映射,扩展或内联函数,以处理提供的类型映射未涵盖的情况.请注意,这些宏和函数经过专门编码,可与 NumPy C/API 一起使用,而与 NumPy 版本号无关,无论是在 1.6 版之后 API 的某些方面被弃用之前还是之后.