C语言 SWIG Python包装器:有条件地导入numpy



我的目标是用Python配置SWIG

  • 如果numpy版本可用,则导入numpy;
  • 在缺少特定numpy版本时不导入numpy。当用户调用支持numpy的API函数时,应该发出错误消息。

我尝试用以下方式整合numpySWIG:

%include "numpy.i"
%init %{
import_array();
%}
%apply (double* IN_ARRAY1, int DIM1) {(const double* mymatrix, int n)};

但是当我导入SWIG生成的Python模块时,我得到以下错误:

numpy.core.multiarray failed to import

据我所知,这个问题可以通过更新numpy包到最新版本来解决(目前我安装了1.12.1)。

但是我不能期望每个使用我的API的人都做同样的事情(安装/更新numpy),特别是当有人不想使用那个API函数时。

所以这绝对是可以做到的。假设目标是构建一次共享对象/DLL并在多个系统上运行它,其中一些系统将安装适当的numpy版本。我们的目标是使numpy特定的功能在运行时没有numpy支持时优雅地出错。

要实现这个目标,我们需要解决两个问题:

  1. 我们必须确保我们的模块仍然可以成功导入
  2. 我们必须确保numpy特定的功能要么完全缺失,要么在导入numpy功能失败时优雅地失败。

我们可以用许多不同的方法来做到这一点。首先,我定义了一些用于测试的功能,其中run.py为:

import test
test.printit() # Must always work
print(repr(test.range(100))) # Must either fail gracefully or succeed if numpy usable
这些函数在我的测试中实现为:
%inline %{
static void printit(void) {
fprintf(stderr, "Hello worldn");
}
static void range(int *rangevec, int n)
{
int i;
for (i=0; i< n; i++)
rangevec[i] = i;
}
%}

其中range直接来自一些numpy文档

单个模块,类型映射检查

我想到的第一个想法是在我们的模块init中使用一些C API代码来封装对import_array的调用,这些代码可以测试异常并清除它,但如果发生异常则设置一个标志。

我们可以使用Python的C API的异常处理来解决问题#1:
%{
#define SWIG_FILE_WITH_INIT
static int have_numpy;
%}
%include "numpy.i"
%init %{
// Try and run import_array() - at build time we need it to be defined
_import_array();
// But at runtime we check if it worked or not:
if (PyErr_Occurred()) {
// Exception occurred during import of numpy
have_numpy = 0;
PyErr_Clear();
}
else {
have_numpy = 1;
}
fprintf(stderr, "Runtime numpy check: %dn", have_numpy);
%}

请注意,这里我们必须调用_import_array而不是import_array,因为在我的一些测试中,当导入失败时,import_array也会捕获异常并打印大量无用的冗长内容。

这一切都很好,但是如果没有解决问题2的解决方案,当numpy导入失败时,我们将无法优雅地失败:

Runtime numpy check: 0
Hello world
Segmentation fault

哇!因此,我们需要一种方法来钩入range的调用,而不是printit,然后很好地引发异常。对于第一个案例,我看了一些东西。最初我尝试使用%exception,因为它有很好的函数匹配语法,但是你得到的钩子在调用中太晚了,我们已经使用了numpy typemaps和爆炸,然后我们可以阻止它使用。

相反,我使用-debug-tmsearch参数来选择一个满足两个属性的类型映射:首先,numpy本身没有使用它(这使事情更简单),其次,它在调用的早期发生。default类型映射是一个很好的候选,因此我们可以在满足我们初始目标的完整SWIG接口中很好地使用它(使用宏来避免重复):

%module test
%{
#define SWIG_FILE_WITH_INIT
static int have_numpy;
%}
%include "numpy.i"
%init %{
// Try and run import_array() - at build time we need it to be defined
_import_array();
// But at runtime we check if it worked or not:
if (PyErr_Occurred()) {
// Exception occurred during import of numpy
have_numpy = 0;
PyErr_Clear();
}
else {
have_numpy = 1;
}
fprintf(stderr, "Runtime numpy check: %dn", have_numpy);
%}
// Handle this gracefully
%define %requires_numpy(fn)
%typemap(default) fn {
if (!have_numpy) {
PyErr_SetString(PyExc_NotImplementedError, "Unimplemented without numpy runtime");
SWIG_fail;
}
}
%enddef
// For testing below:
// This must be before the %apply or typemap use
%requires_numpy((int *ARGOUT_ARRAY1, int DIM1));
%apply (int* ARGOUT_ARRAY1, int DIM1) {(int* rangevec, int n)}
%inline %{
static void printit(void) {
fprintf(stderr, "Hello worldn");
}
static void range(int *rangevec, int n)
{
int i;
for (i=0; i< n; i++)
rangevec[i] = i;
}
%}

如果没有numpy,我们现在从run.py得到这个:

Runtime numpy check: 0
Hello world
Traceback (most recent call last):
File "run.py", line 5, in <module>
print(repr(test.range(100)))
NotImplementedError: Unimplemented without numpy runtime

使用numpy我们得到:

Hello world
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33,
34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67,
68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84,
85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99],
dtype=int32)

单个模块,附加Python代码

我们也可以在Python中实现异常抛出,如果我们愿意,可以使用%pythonprepend。这与以前相同,除了我们使用Python引发异常并将have_numpy暴露给Python模块以供其使用:

%module test
%{
#define SWIG_FILE_WITH_INIT
static int have_numpy;
%}
const int have_numpy;
%include "numpy.i"
%init %{
// Try and run import_array() - at build time we need it to be defined
_import_array();
// But at runtime we check if it worked or not:
if (PyErr_Occurred()) {
// Exception occurred during import of numpy
have_numpy = 0;
PyErr_Clear();
}
else {
have_numpy = 1;
}
fprintf(stderr, "Runtime numpy check: %dn", have_numpy);
%}
// Handle this gracefully
%define %requires_numpy(fn)
%pythonprepend fn %{
if not have_numpy: raise NotImplementedError("This requires numpy")
%}
%enddef
// For testing below:
%requires_numpy(range);
%apply (int* ARGOUT_ARRAY1, int DIM1) {(int* rangevec, int n)}
%inline %{
static void printit(void) {
fprintf(stderr, "Hello worldn");
}
static void range(int *rangevec, int n)
{
int i;
for (i=0; i< n; i++)
rangevec[i] = i;
}
%}

我更喜欢这样,因为这意味着我们现在可以命名需要numpy的函数,而不是干扰它们的类型映射。

拆分为两个模块,交叉导入

最后,另一种选择是将我们的模块分成两个独立的模块。通过将numpy特定的函数放入另一个模块并尝试导入,我们仍然可以实现相同的行为。我们可以使用%import在两个模块之间共享一些类型和代码。我还没有完成这个例子,因为它(可能?)比你想要的更复杂,但是一个大纲应该是这样的:

%module test
%inline %{
static void printit(void) {
fprintf(stderr, "Hello worldn");
}
%}
%pythoncode %{
try:
from test_numpy import *
except:
pass # This is fine
%}

%module test_numpy
%import "test.i"
%{
#define SWIG_FILE_WITH_INIT
%}
%include "numpy.i"
%init %{
// Try and run import_array() - at build time we need it to be defined
_import_array();
%}
%apply (int* ARGOUT_ARRAY1, int DIM1) {(int* rangevec, int n)}
%inline %{
static void range(int *rangevec, int n)
{
int i;
for (i=0; i< n; i++)
rangevec[i] = i;
}
%}

类似这样的内容应该可以作为前两个选项的替代方案,但目前尚未经过测试。

最新更新