SWIG Numpy 示例

日期2009-08-11(最后修改),2008-12-31(创建)

简介

这些是使用 numpy.i 接口文件的简单 !NumPy 和 SWIG 示例。对于希望在 Win32 环境中使用这些示例的人员,还提供了一个 MinGW 部分。以下代码是 C 语言,而不是 C++。

此处包含的信息最初由 Bill Spotz 在其文章numpy.i: a SWIG Interface File for !NumPy中提供,以及可以通过以下命令签出的 !NumPy SVN

In [ ]
svn co http://scipy.org/svn/numpy/trunk numpy

* The !NumPy+SWIG manual is available here:1\ * The numpy.i file can be downloaded from the SVN:2\ * and the pyfragments.swg file, hich is also needed, is available from3\ * These two files (and others) are also available in the numpy source tarball:4

初始设置

gcc 和 SWIG

检查 gcc 和 SWIG 是否可用(已知路径)

In [ ]
swig -version

In [ ]
gcc -v

两者都应该输出一些文本...

修改 pyfragments.swg 文件(仅限 MinGW)

这是我自己的测试结果,运行 SWIG 版本 1.3.36 和 gcc 版本 3.4.5(mingw-vista 特别版 r3)。我必须从源代码中删除“static”语句,否则你的 SWIGed 源代码将无法编译。该文件中只有两个“static”语句,都需要删除。这是我修改后的版本:pyfragments.swg

编译和测试

首先需要为每个模块编写一个特定的 setup.py 文件。我参考了 https://scipy.org.cn/svn/numpy/trunk/doc/swig/test/ 中的参考 setup.py 文件,并添加了自动处理 swig 的功能。

在类 Unix 系统上,命令行是

In [ ]
python setup.py build

在 Win32 环境(cygwin 或 cmd)中,设置命令行是(用于 MinGW)

In [ ]
python setup.py build --compiler=mingw32

该命令同时处理 SWIG 过程(生成包装 C 和 Python 代码)和 gcc 编译。生成的模块(一个 pyd 文件)将构建在 `build\lib.XXX` 目录中(例如,对于 Python 2.5 安装和 Win32 机器,则为 `build\lib.win32-2.5` 目录)。

一个简单的 ARGOUT_ARRAY1 示例

这是一个 range 函数的重新实现。该模块称为 ezrange。使用 `ARGOUT_ARRAY1` 时要记住的一件事是,必须从 Python 传递数组的维度。

来自 Bill Spotz 的文章:Python 用户不会传入这些数组,它们只是被返回。对于指定维度的案例,Python 用户必须提供该维度作为参数。

这对于像 `numpy.arange(N)` 这样的函数很有用,因为返回数组的大小事先已知并传递给 C 函数。

对于遵循 `array_out = function(array_in)` 的函数,其中 array_out 的大小事先未知,并且取决于在 C 中分配的内存,请参见 [:Cookbook/SWIG Memory Deallocation] 中给出的示例。

C 源代码(ezrange.c 和 ezrange.h)

这是 ezrange.h 文件

In [ ]
void range(int *rangevec, int n);

这是 ezrange.c 文件

In [ ]
void range(int *rangevec, int n)
{
    int i;

    for (i=0; i< n; i++)
        rangevec[i] = i;
}

接口文件(ezrange.i)

这是 ezrange.i 文件。

In [ ]
%module ezrange

%{
    #define SWIG_FILE_WITH_INIT
    #include "ezrange.h"
%}

%include "numpy.i"

%init %{
    import_array();
%}

%apply (int* ARGOUT_ARRAY1, int DIM1) {(int* rangevec, int n)}

%include "ezrange.h"

不要忘记,您还需要在同一个目录中使用 numpy.i 文件。

设置文件(setup.py)

这是我的 setup.py 文件

In [ ]
#! /usr/bin/env python

# System imports
from distutils.core import *
from distutils      import sysconfig

# Third-party modules - we depend on numpy for everything
import numpy

# Obtain the numpy include directory.  This logic works across numpy versions.
try:
    numpy_include = numpy.get_include()
except AttributeError:
    numpy_include = numpy.get_numpy_include()

# ezrange extension module
_ezrange = Extension("_ezrange",
                   ["ezrange.i","ezrange.c"],
                   include_dirs = [numpy_include],
                   )

# ezrange setup
setup(  name        = "range function",
        description = "range takes an integer and returns an n element int array where each element is equal to its index",
        author      = "Egor Zindy",
        version     = "1.0",
        ext_modules = [_ezrange]
        )

编译模块

设置命令行是

In [ ]
python setup.py build

或者

In [ ]
python setup.py build --compiler=mingw32

取决于您的环境。

测试模块

如果一切按计划进行,应该在 `build\lib.XXX` 目录中有一个 `_ezrange.pyd` 文件。您需要将该文件复制到 `ezrange.py` 文件所在的目录(由 swig 生成),在这种情况下,以下操作将起作用(在 python 中)

In [ ]
>>> import ezrange
>>> ezrange.range(10)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

一个简单的 INPLACE_ARRAY1 示例

此示例将传递给它的 1-D 数组的元素加倍。操作是在原地完成的,这意味着传递给函数的数组将被更改。

C 源代码(inplace.c 和 inplace.h)

这是 inplace.h 文件

In [ ]
void inplace(double *invec, int n);

这是 inplace.c 文件

In [ ]
void inplace(double *invec, int n)
{
    int i;

    for (i=0; i<n; i++)
    {
        invec[i] = 2*invec[i];
    }
}

接口文件(inplace.i)

这是 inplace.i 接口文件

In [ ]
%module inplace

%{
    #define SWIG_FILE_WITH_INIT
    #include "inplace.h"
%}

%include "numpy.i"

%init %{
    import_array();
%}

%apply (double* INPLACE_ARRAY1, int DIM1) {(double* invec, int n)}
%include "inplace.h"

设置文件(setup.py)

这是我的 setup.py 文件

In [ ]
#! /usr/bin/env python

# System imports
from distutils.core import *
from distutils      import sysconfig

# Third-party modules - we depend on numpy for everything
import numpy

# Obtain the numpy include directory.  This logic works across numpy versions.
try:
    numpy_include = numpy.get_include()
except AttributeError:
    numpy_include = numpy.get_numpy_include()

# inplace extension module
_inplace = Extension("_inplace",
                   ["inplace.i","inplace.c"],
                   include_dirs = [numpy_include],
                   )

# NumyTypemapTests setup
setup(  name        = "inplace function",
        description = "inplace takes a double array and doubles each of its elements in-place.",

        author      = "Egor Zindy",
        version     = "1.0",
        ext_modules = [_inplace]
        )

编译模块

设置命令行是

In [ ]
python setup.py build

或者

In [ ]
python setup.py build --compiler=mingw32

取决于您的环境。

测试模块

如果一切按计划进行,应该在 `build\lib.XXX` 目录中有一个 `_inplace.pyd` 文件。您需要将该文件复制到 `inplace.py` 文件所在的目录(由 swig 生成),在这种情况下,以下操作将起作用(在 python 中)

In [ ]
>>> import numpy
>>> import inplace
>>> a = numpy.array([1,2,3],'d')
>>> inplace.inplace(a)
>>> a
array([2., 4., 6.])

一个简单的 ARGOUTVIEW_ARRAY1 示例

大量警告

请注意,Bill Spotz 建议不要使用 argout_view 数组,除非绝对必要

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

正如 Travis Oliphant 在这里所说,Python 不负责内存释放:1

然而,棘手的部分是内存管理。内存是如何释放的?建议一直都是类似于“确保内存不会在 !NumPy 数组消失之前被释放”。这是一个很好的建议,但通常没有帮助,因为它基本上只是告诉您创建一个内存泄漏。

内存释放也很难自动处理,因为没有简单的方法来进行模块“终结”。有一个 `Py_InitModule()` 函数,但没有用于处理删除/销毁/终结的函数(这将在 Python 3000 中得到解决,如 PEP3121 中所述)。在我的示例中,我使用 python 模块 atexit,但一定有更好的方法。

话虽如此,如果你别无选择,这里有一个使用 ARGOUTVIEW_ARRAY1 的示例。和往常一样,欢迎评论!

该模块声明了一块内存和几个函数:* ezview.set_ones() 将内存块中的所有元素(双精度浮点数)设置为 1,并返回一个指向该内存块的视图的 numpy 数组。* ezview.get_view() 只是返回内存块的视图。* ezview.finalize() 负责内存释放(这是此示例的薄弱环节)。

C 源代码 (ezview.c 和 ezview.h)

这是 ezview.h 文件

In [ ]
void set_ones(double *array, int n);

这是 ezview.c 文件

In [ ]
#include <stdio.h>
#include <stdlib.h> 

#include "ezview.h"

void set_ones(double *array, int n)
{
    int i;

    if (array == NULL)
        return;

    for (i=0;i<n;i++)
        array[i] = 1.;
}

接口文件 (ezview.i)

这是 ezview.i 接口文件

In [ ]
%module ezview

%{
    #define SWIG_FILE_WITH_INIT
    #include "ezview.h"

    double *my_array = NULL;
    int my_n = 10;

    void __call_at_begining()
    {
        printf("__call_at_begining...\n");
        my_array = (double *)malloc(my_n*sizeof(double));
    }

    void __call_at_end(void)
    {
        printf("__call_at_end...\n");
        if (my_array != NULL)
            free(my_array);
    }
%}

%include "numpy.i"

%init %{
    import_array();
    __call_at_begining();
%}

%apply (double** ARGOUTVIEW_ARRAY1, int *DIM1) {(double** vec, int* n)}

%include "ezview.h"
%rename (set_ones) my_set_ones;

%inline %{
void finalize(void){
    __call_at_end();
}

void get_view(double **vec, int* n) {
    *vec = my_array;
    *n = my_n;
}

void my_set_ones(double **vec, int* n) {
    set_ones(my_array,my_n);
    *vec = my_array;
    *n = my_n;
}
%}

不要忘记,你还需要在同一个目录中使用 numpy.i 文件。

设置文件(setup.py)

这是我的 setup.py 文件

In [ ]
#! /usr/bin/env python

# System imports
from distutils.core import *
from distutils      import sysconfig

# Third-party modules - we depend on numpy for everything
import numpy

# Obtain the numpy include directory.  This logic works across numpy versions.
try:
    numpy_include = numpy.get_include()
except AttributeError:
    numpy_include = numpy.get_numpy_include()

# view extension module
_ezview = Extension("_ezview",
                   ["ezview.i","ezview.c"],
                   include_dirs = [numpy_include],
                   )

# NumyTypemapTests setup
setup(  name        = "ezview module",
        description = "ezview provides 3 functions: set_ones(), get_view() and finalize(). set_ones() and get_view() provide a view on a memory block allocated in C, finalize() takes care of the memory deallocation.",
        author      = "Egor Zindy",
        version     = "1.0",
        ext_modules = [_ezview]
        )

编译模块

设置命令行是

In [ ]
python setup.py build

或者

In [ ]
python setup.py build --compiler=mingw32

取决于您的环境。

测试模块

如果一切按计划进行,应该在 `build\lib.XXX` 目录中有一个 `_ezview.pyd` 文件。你需要将该文件复制到 `ezview.py` 文件所在的目录(由 swig 生成),在这种情况下,以下操作将在 python 中起作用

测试代码 test_ezview.py 如下

In [ ]
import atexit
import numpy
print "first message is from __call_at_begining()"
import ezview

#There is no easy way to finalize the module (see PEP3121)
atexit.register(ezview.finalize)

a = ezview.set_ones()
print "\ncalling ezview.set_ones() - now the memory block is all ones.\nReturned array (a view on the allocated memory block) is:"
print a

print "\nwe're setting the array using a[:]=arange(a.shape[0])\nThis changes the content of the allocated memory block:"
a[:] = numpy.arange(a.shape[0])
print a

print "\nwe're now deleting the array  - this only deletes the view,\nnot the allocated memory!"
del a

print "\nlet's get a new view on the allocated memory, should STILL contain [0,1,2,3...]"
b = ezview.get_view()
print b

print "\nnext message from __call_at_end() - finalize() registered via module atexit"

启动 test_ezview.py,希望会发生以下情况

In [ ]
~> python test_ezview.py
first message is from __call_at_begining()
__call_at_begining...

calling ezview.set_ones() - now the memory block is all ones.
Returned array (a view on the allocated memory block) is:
[ 1.  1.  1.  1.  1.  1.  1.  1.  1.  1.]

we re setting the array using a[:]=arange(a.shape[0])
This changes the content of the allocated memory block:
[ 0.  1.  2.  3.  4.  5.  6.  7.  8.  9.]

we re now deleting the array  - this only deletes the view,
not the allocated memory!

let s get a new view on the allocated memory, should STILL contain [0,1,2,3...]
[ 0.  1.  2.  3.  4.  5.  6.  7.  8.  9.]

next message from __call_at_end() - finalize() registered via module atexit
__call_at_end...

使用 errno 和 python 异常进行错误处理

我已经测试了几个月,这是我想到的最好的方法。如果有人知道更好的方法,请告诉我。

从 opengroup 网站上看,左值 errno 被许多函数用来返回错误值。其思想是在调用函数之前将全局变量 errno 设置为 0(在 swig 语法中:\$action),并在之后检查。如果 errno 非零,则会根据 errno 的值生成带有有意义消息的 python 异常。

以下示例包含两个示例:第一个示例在检查数组索引是否有效时使用 errno。第二个示例使用 errno 通知用户 malloc() 问题。

C 源代码 (ezerr.c 和 ezerr.h)

这是 ezerr.h 文件

In [ ]
int val(int *array, int n, int index);
void alloc(int n);

这是 ezerr.c 文件

In [ ]
#include <stdlib.h>
#include <errno.h>

#include "ezerr.h"

//return the array element defined by index
int val(int *array, int n, int index)
{
    int value=0;

    if (index < 0 || index >=n)
    {
        errno = EPERM;
        goto end;
    }

    value = array[index];

end:
    return value;
}

//allocate (and free) a char array of size n
void alloc(int n)
{
    char *array;

    array = (char *)malloc(n*sizeof(char));
    if (array == NULL)
    {
        errno = ENOMEM;
        goto end;
    }

    //don't keep the memory allocated...
    free(array);

end:
    return;
}

接口文件 (ezerr.i)

这是 ezerr.i 接口文件

In [ ]
%module ezerr
%{
#include <errno.h>
#include "ezerr.h"

#define SWIG_FILE_WITH_INIT
%}

%include "numpy.i"

%init %{
    import_array();
%}

%exception
{
    errno = 0;
    $action

    if (errno != 0)
    {
        switch(errno)
        {
            case EPERM:
                PyErr_Format(PyExc_IndexError, "Index out of range");
                break;
            case ENOMEM:
                PyErr_Format(PyExc_MemoryError, "Failed malloc()");
                break;
            default:
                PyErr_Format(PyExc_Exception, "Unknown exception");
        }
        SWIG_fail;
    }
}

%apply (int* IN_ARRAY1, int DIM1) {(int *array, int n)}

%include "ezerr.h"

请注意SWIG_fail,它是一个用于goto fail的宏,以防需要执行其他清理代码(感谢Bill!)。

不要忘记,你还需要在同一个目录中使用 numpy.i 文件。

设置文件(setup.py)

这是我的setup.py文件

In [ ]
#! /usr/bin/env python

# System imports
from distutils.core import *
from distutils      import sysconfig

# Third-party modules - we depend on numpy for everything
import numpy

# Obtain the numpy include directory.  This logic works across numpy versions.
try:
    numpy_include = numpy.get_include()
except AttributeError:
    numpy_include = numpy.get_numpy_include()

# err extension module
ezerr = Extension("_ezerr",
                   ["ezerr.i","ezerr.c"],
                   include_dirs = [numpy_include],

                   extra_compile_args = ["--verbose"]
                   )

# NumyTypemapTests setup
setup(  name        = "err test",
        description = "A simple test to demonstrate the use of errno and python exceptions",
        author      = "Egor Zindy",
        version     = "1.0",
        ext_modules = [ezerr]
        )

编译模块

设置命令行是

In [ ]
python setup.py build

或者

In [ ]
python setup.py build --compiler=mingw32

取决于您的环境。

测试模块

如果一切按计划进行,在`build\lib.XXX`目录中应该有一个`_ezerr.pyd`文件。您需要将该文件复制到`ezerr.py`文件所在的目录(由swig生成),在这种情况下,以下操作将起作用(在python中)

测试代码test_err.py如下

In [ ]
import traceback,sys
import numpy
import ezerr

print "\n--- testing ezerr.val() ---"
a = numpy.arange(10)
indexes = [5,20,-1]

for i in indexes:
    try:
        value = ezerr.val(a,i)
    except:
        print ">> failed for index=%d" % i
        traceback.print_exc(file=sys.stdout)
    else:
        print "using ezerr.val() a[%d]=%d - should be %d" % (i,value,a[i])

print "\n--- testing ezerr.alloc() ---"
amounts = [1,-1] #1 byte, -1 byte

for n in amounts:
    try:
        ezerr.alloc(n)
    except:
        print ">> could not allocate %d bytes" % n
        traceback.print_exc(file=sys.stdout)
    else:
        print "allocated (and deallocated) %d bytes" % n

启动test_err.py,以下操作将有望发生

In [ ]
~> python test_err.py

--- testing ezerr.val() ---
using ezerr.val() a[5]=5 - should be 5
>> failed for index=20
Traceback (most recent call last):
  File "test_err.py", line 11, in <module>
    value = ezerr.val(a,i)
IndexError: Index out of range
>> failed for index=-1
Traceback (most recent call last):
  File "test_err.py", line 11, in <module>
    value = ezerr.val(a,i)
IndexError: Index out of range

--- testing ezerr.alloc() ---
allocated (and deallocated) 1 bytes
>> could not allocate -1 bytes
Traceback (most recent call last):
  File "test_err.py", line 23, in <module>
    ezerr.alloc(n)
MemoryError: Failed malloc()

点积示例(来自Bill Spotz的文章)

Bill Spotz文章中给出的最后一个示例是点积函数。这是一个完整的版本。

C源代码(dot.c和dot.h)

这是dot.h文件

In [ ]
double dot(int len, double* vec1, double* vec2);

这是dot.c文件

In [ ]
#include <stdio.h>
#include "dot.h"

double dot(int len, double* vec1, double* vec2)
{
    int i;
    double d;

    d = 0;
    for(i=0;i<len;i++)
        d += vec1[i]*vec2[i];

    return d;
}

接口文件(dot.i和numpy.i)

这是完整的dot.i文件

In [ ]
%module dot

%{
    #define SWIG_FILE_WITH_INIT
    #include "dot.h"
%}

%include "numpy.i"

%init %{
    import_array();
%}

%apply (int DIM1, double* IN_ARRAY1) {(int len1, double* vec1), (int len2, double* vec2)}


%include "dot.h"
%rename (dot) my_dot;

%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);
}
%}

设置文件(setup.py)

这是setup.py文件

In [ ]
#! /usr/bin/env python

# System imports
from distutils.core import *
from distutils      import sysconfig

# Third-party modules - we depend on numpy for everything
import numpy

# Obtain the numpy include directory.  This logic works across numpy versions.
try:
    numpy_include = numpy.get_include()
except AttributeError:
    numpy_include = numpy.get_numpy_include()

# dot extension module
_dot = Extension("_dot",
                   ["dot.i","dot.c"],
                   include_dirs = [numpy_include],
                   )

# dot setup
setup(  name        = "Dot product",
        description = "Function that performs a dot product (numpy.i: a SWIG Interface File for NumPy)",
        author      = "Egor Zindy (based on the setup.py file available in the numpy tree)",
        version     = "1.0",
        ext_modules = [_dot]
        )

编译模块

设置命令行是

In [ ]
python setup.py build

或者

In [ ]
python setup.py build --compiler=mingw32

取决于您的环境。

测试

如果一切按计划进行,在`build\lib.XXX`目录中应该有一个`_dot.pyd`文件。您需要将该文件复制到`dot.py`文件所在的目录(由swig生成),在这种情况下,以下操作将起作用(在python中)

In [ ]
>>> import dot
>>> dot.dot([1,2,3],[1,2,3])
14.0

结论

这就是全部内容(暂时)!和往常一样,欢迎评论!

  • '''TODO''': 代码清理并将示例移至!SciPy/!NumPy存储库?

此致,Egor

章节作者:EgorZindy