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

简介#

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

double rms(double* seq, int n);

究竟什么是 seq。它是一个要被就地修改的单个值吗?它是一个数组,如果是的话,它的长度是多少?它是仅供输入的?仅供输出的?输入输出? SWIG 无法确定这些细节,也不会尝试这样做。

如果我们设计了 rms,我们可能把它做成一个例程,该例程接受一个名为 seq 的长度为 n 的仅供输入的 double 值数组,并返回均方根。然而,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 数组的(可能的)转换。这些函数在 辅助函数 部分进行了说明,但希望它们的名称不言自明。在第 12 行,我们使用 obj0 来构造一个 NumPy 数组。在第 17 行,我们检查结果的有效性:它不为空且只有一个任意长度的维度。一旦验证了这些状态,我们就在第 19 行和第 20 行中提取数据缓冲区和长度,以便我们可以在第 22 行调用底层的 C 函数。第 25 行为我们已经创建不再需要的数组执行内存管理。

此代码包含大量的错误处理。请注意 SWIG_failgoto 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 顺序(或一维)数组。

输入数组#

输入数组被定义为传递到例程但不会被就地修改或返回给用户的数组。因此,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 视图数组#

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

要为 argoutview 数组包装的 C 代码以指针为特征:指向维度的指针和指向数据的双指针,以便这些值可以传递回用户。因此,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 视图数组#

最近添加到 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 编写的,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_complexnpy_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 包装的 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 使用“先到先得”的系统来确定片段的活动定义。也就是说,我们需要在 SWIG 在内部执行之前定义 long 转换的片段。SWIG 允许我们通过将片段定义放在 pyfragments.swg 文件中来实现这一点。如果我们将新的片段定义放在 numpy.i 中,它们将被忽略。

辅助函数#

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

%fragment("NumPy_Fragments");

在您的 SWIG 接口文件中。

#

is_array(a)

如果 a 不为 NULL 并且可以转换为 PyArrayObject*,则计算结果为 true。

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)

计算 a 的第 i 个维度的尺寸,假设 a 可以转换为 PyArrayObject*

array_strides(a)

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

array_stride(a,i)

计算 a 的第 i 个步长,假设 a 可以转换为 PyArrayObject*

array_data(a)

计算一个类型为 void 的指针,该指针指向 a 的数据缓冲区,假设 a 可以转换为 PyArrayObject*

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 是一个连续数组,则计算结果为 true。等效于 (PyArray_ISCONTIGUOUS(a))

array_is_native(a)

如果 a 的数据缓冲区使用本机字节顺序,则计算结果为 true。等效于 (PyArray_ISNOTSWAPPED(a))

array_is_fortran(a)

如果 a 是 FORTRAN 顺序的,则计算结果为 true。

例程#

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。如果 input 无法转换,或者 typecode 错误,则设置 Python 错误并返回 NULL

obj_to_array_allow_conversion()

返回类型:PyArrayObject*

参数

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

  • int typecode,生成的数组的所需 NumPy 类型代码。

  • int* is_new_object,如果未执行转换,则返回 0,否则返回 1。

input 转换为具有给定 typecode 的 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 nexact_dimensions 的长度。

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

require_size()

返回类型:int

参数

  • PyArrayObject* ary,一个 NumPy 数组。

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

  • int nsize 的长度。

要求 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 错误。生成的包装器函数将在向量长度不匹配时返回 Python 浮点数表示的 0.0。由于这不是 NULL,因此 Python 解释器将不知道检查错误。出于这个原因,我们在上面的 my_dot 中添加了 %exception 指令以获得我们想要的行为(请注意,$action 是一个宏,扩展为对 my_dot 的有效调用)。一般来说,您可能需要编写一个 SWIG 宏来执行此任务。

其他情况#

在遇到这些情况时,还有其他包装情况,其中 numpy.i 可能会有所帮助。

  • 在某些情况下,可以使用 %numpy_typemaps 宏来实现您自己类型的类型映射。有关示例,请参阅 其他常见类型:bool其他常见类型: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 charunsigned charshortunsigned shortintunsigned intlongunsigned longlong longunsigned long longfloatdouble

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

    • 一维、二维、三维和四维数组。

    • 仅输入、就地、argout、argoutview 和内存管理 argoutview 行为。

    • 硬编码维度、数据缓冲区然后维度规范以及维度然后数据缓冲区规范。

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

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

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

  • 14 个 C 宏和 15 个 C 函数,可用于编写专门的类型映射、扩展或内联函数,以处理提供的类型映射未涵盖的情况。请注意,宏和函数是专门编码的,以便与 NumPy C/API 配合使用,无论 NumPy 版本号如何,包括在 1.6 版本之后弃用 API 的某些方面之前和之后。