将numpy数组传递给需要动态分配数组的C函数的Cython方法

8

我有一些C代码,其中有以下声明:

int myfunc(int m, int n, const double **a, double **b, double *c);

所以a是一个常量二维数组,b是一个二维数组,c是一个一维数组,它们都是动态分配的。在传递给myfunc之前,bc不需要特别的内容,并且应该被理解为输出信息。对于这个问题,我不允许改变myfunc的声明。

问题1:如何将给定的numpy数组a_np转换为这个C函数所需的格式的数组a,以便我可以在Cython中使用a调用此C函数?

问题2:bc的声明是否正确,或者它们需要以其他格式来使C函数能够将它们理解为2D和1D数组(分别)?

我的尝试:

myfile.pxd

cdef extern from "myfile.h":
    int myfunc(int p, int q, const double **a, double **b, double *c)

mytest.pyx

cimport cython
cimport myfile
import numpy as np
cimport numpy as np

p = 3
q = 4
cdef:
    double** a = np.random.random([p,q])
    double** b
    double* c

myfile.myfunc(p, q, a, b, c)

然后在iPython中运行以下命令:

import pyximport; pyximport.install()
import mytest

定义变量a的那行代码会提示错误信息:无法将Python对象转换为'double **'。关于变量bc没有出现任何错误信息,但由于我目前无法运行C函数,因此不确定变量bc的声明是否正确(即能使C函数分别输出2D和1D数组)。

其他尝试:我还尝试了遵循这里的解决方案,但这不能处理我在myfunc声明中使用的双星号类型的数组。 这里的解决方案不适用于我的任务,因为我无法更改myfunc的声明。


当你说“动态分配”时,你的意思是在 myfunc 之外?而且由于你试图将 numpy 数组传递给 myfunc,所以这是无关紧要的,你只需要将这些 numpy 数组转换为适当的参数格式(double 和 single 指向 double 的指针),对吗? - user707650
@Evert 首先,让我警告你我对 C 不是很了解。我只是在尝试使用 myfunc 计算数组 bc,我不需要它们是动态分配的或者有什么特殊的东西。我之所以称它们为“动态分配”,只是因为我认为双指针和单指针到 double 都需要这种格式。简而言之,是的,你是正确的。 - Alex
使用 double** 与 numpy 不太匹配。请参见 https://dev59.com/614c5IYBdhLWcg3wqLte 进行讨论。 - DavidW
2个回答

12

在Cython中创建一个帮助数组

为了从numpy数组中获取double **,您可以在*.pyx文件中创建一个指针的辅助数组。此外,您必须确保numpy数组具有正确的内存布局。(这可能涉及创建副本)

Fortran顺序

如果您的C函数需要Fortran顺序(所有x坐标在一个列表中,所有y坐标在另一个列表中,所有z坐标在第三个列表中,如果您的数组a对应于3D空间中点的列表)

N,M = a.shape
# Make sure the array a has the correct memory layout (here F-order)
cdef np.ndarray[double, ndim=2, mode="fortran"] a_cython =
                         np.asarray(a, dtype = float, order="F")
#Create our helper array
cdef double** point_to_a = <double **>malloc(M * sizeof(double*))
if not point_to_a: raise MemoryError
try:
    #Fillup the array with pointers
    for i in range(M): 
        point_to_a[i] = &a_cython[0, i]
    # Call the C function that expects a double**
    myfunc(... &point_to_a[0], ...)
finally:
    free(point_to_a)

C-order

如果您的C函数期望C-order([x1,y1,z1]是第一个列表,[x2,y2,z2]是第二个列表,用于表示3D点的列表):

N,M = a.shape
# Make sure the array a has the correct memory layout (here C-order)
cdef np.ndarray[double, ndim=2, mode="c"] a_cython =
                         np.asarray(a, dtype = float, order="C")
#Create our helper array
cdef double** point_to_a = <double **>malloc(N * sizeof(double*))
if not point_to_a: raise MemoryError
try:
    for i in range(N): 
        point_to_a[i] = &a_cython[i, 0]
    # Call the C function that expects a double**
    myfunc(... &point_to_a[0], ...)
finally:
    free(point_to_a)

你不知道我寻找这个答案已经多久了。非常感谢您发布它。但是a_cython在哪里定义的? - Matt
cdef np.ndarray[double, ndim=2, mode="fortran"] a_cython = np.asarray(a, dtype = float, order="F") - Bernhard
谢谢,我可能没有在 SO 应用程序上滚动到足够远的位置。 - Matt
2
非常有帮助的答案,谢谢 +1!请注意,Cython文档建议使用PyMem_Malloc()PyMem_Free()代替malloc()free() - normanius
2
此外,使用更通用的类型化内存视图(例如cdef [:,::1] a_view = np.ascontiguousarray(a)而不是cdef np.ndarray[.....] a_view = ...)具有更好的可读性和其他优点。另请参阅此教程此(重复)帖子 - normanius

0

回复1:您可以使用以下代码将NumPy数组通过Cython传递到C,使用数组的起始位置。

回复2:您的声明似乎是正确的,但我不使用这种显式内存管理的方法。您可以使用NumPy来声明-ed数组。

使用

cdef double[:,::1] a = np.random.random([p, q])
cdef double[:,::1] b = np.empty([p, q])
cdef double[::1] b = np.empty(q)

然后将数组起始位置的&a[0]传递给您的C函数。 ::1是为了确保连续性。

Jake Vanderplas的博客是一个很好的参考:https://jakevdp.github.io/blog/2012/08/08/memoryview-benchmarks/

最后,通常在Cython中创建函数并在Python中调用它们,因此您的Python代码将是:

import pyximport; pyximport.install()
import mytest
mytest.mywrappedfunc()

其中mywrappedfunc是一个Python函数(使用def而不是cdef), 它在模块中定义,可以执行上述数组声明。


谢谢您的回复,但这并不起作用...我收到以下错误:对于数组a无法获取memoryview切片的地址,对于b无法将类型'double[:, ::1]'分配给'double **',对于c无法将类型'double[::1]'分配给'double *' - Alex
是的,我用你提供的cdef语句替换了我的cdef语句。 - Alex
1
嗨,我理解我的困惑。我使用了cythonized Fortran代码,因此它有效。你在C中使用的是一个二维数组,它是指向一维数组的指针的一维数组,而在Fortran中,你有一个直接指向数组开头的指针作为参数。选项:1. 在编译时设置C数组的所有(除第一个外)维度。2. 在C中使用具有手动“子索引”的一维数组来存储数据。3. 如果您无法更改C代码,则必须在Cython级别上将数组构建为“指向一维数组的指针数组”,并传递此数组。语法&c[0]应该可以工作,我刚刚检查过了。 - Pierre de Buyl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接