强制NumPy ndarray在Cython中获取其内存的所有权

时间:2014-05-26 15:00:16

标签: python arrays numpy cython

关注this answer to "Can I force a numpy ndarray to take ownership of its memory?"我试图通过Cython的NumPy包装器使用Python C API函数PyArray_ENABLEFLAGS,发现它没有暴露。

以下尝试手动公开它(这只是再现故障的最小例子)

from libc.stdlib cimport malloc
import numpy as np
cimport numpy as np

np.import_array()

ctypedef np.int32_t DTYPE_t

cdef extern from "numpy/ndarraytypes.h":
    void PyArray_ENABLEFLAGS(np.PyArrayObject *arr, int flags)

def test():
    cdef int N = 1000

    cdef DTYPE_t *data = <DTYPE_t *>malloc(N * sizeof(DTYPE_t))
    cdef np.ndarray[DTYPE_t, ndim=1] arr = np.PyArray_SimpleNewFromData(1, &N, np.NPY_INT32, data)
    PyArray_ENABLEFLAGS(arr, np.NPY_ARRAY_OWNDATA)

因编译错误而失败:

Error compiling Cython file:
------------------------------------------------------------
...
def test():
    cdef int N = 1000

    cdef DTYPE_t *data = <DTYPE_t *>malloc(N * sizeof(DTYPE_t))
    cdef np.ndarray[DTYPE_t, ndim=1] arr = np.PyArray_SimpleNewFromData(1, &N, np.NPY_INT32, data)
    PyArray_ENABLEFLAGS(arr, np.NPY_ARRAY_OWNDATA)
                          ^
------------------------------------------------------------

/tmp/test.pyx:19:27: Cannot convert Python object to 'PyArrayObject *'

我的问题:这是采用这种情况的正确方法吗?如果是这样,我做错了什么?如果没有,我如何强制NumPy取得Cython的所有权,而不需要转到C扩展模块?

3 个答案:

答案 0 :(得分:17)

接口定义中只有一些小错误。以下对我有用:

from libc.stdlib cimport malloc
import numpy as np
cimport numpy as np

np.import_array()

ctypedef np.int32_t DTYPE_t

cdef extern from "numpy/arrayobject.h":
    void PyArray_ENABLEFLAGS(np.ndarray arr, int flags)

cdef data_to_numpy_array_with_spec(void * ptr, np.npy_intp N, int t):
    cdef np.ndarray[DTYPE_t, ndim=1] arr = np.PyArray_SimpleNewFromData(1, &N, t, ptr)
    PyArray_ENABLEFLAGS(arr, np.NPY_OWNDATA)
    return arr

def test():
    N = 1000

    cdef DTYPE_t *data = <DTYPE_t *>malloc(N * sizeof(DTYPE_t))
    arr = data_to_numpy_array_with_spec(data, N, np.NPY_INT32)
    return arr

这是我的setup.py文件:

from distutils.core import setup, Extension
from Cython.Distutils import build_ext
ext_modules = [Extension("_owndata", ["owndata.pyx"])]
setup(cmdclass={'build_ext': build_ext}, ext_modules=ext_modules)

使用python setup.py build_ext --inplace构建。然后验证数据是否实际拥有:

import _owndata
arr = _owndata.test()
print arr.flags

除其他外,您应该看到OWNDATA : True

并且,这绝对是解决这个问题的正确方法,因为numpy.pxd将所有其他函数导出到Cython完全相同。

答案 1 :(得分:4)

最新的Cython版本允许您以最少的语法进行操作,尽管开销比建议的低级解决方案略高。

numpy_array = np.asarray(<np.int32_t[:10, :10]> my_pointer)

https://cython.readthedocs.io/en/latest/src/userguide/memoryviews.html#coercion-to-numpy

仅此一项就无法转移所有权。

值得注意的是,此调用通过array_cwrapper生成了Cython数组。

这将生成一个cython.array,而不分配内存。 cython.array默认使用stdlib.h mallocfree,因此可以预期的是,您也使用默认的malloc,而不是任何特殊的CPython / Numpy分配器

free仅在为此cython.array设置所有权时才调用,默认情况下只有在分配数据时才被调用。对于我们的情况,我们可以通过以下方式手动设置:

my_cyarr.free_data = True


因此,要返回一维数组,将非常简单:

from cython.view cimport array as cvarray

# ...
    cdef cvarray cvarr = <np.int32_t[:N]> data
    cvarr.free_data = True
    return np.asarray(cvarr)

答案 2 :(得分:1)

@Stefan's solution适用于大多数情况,但有些脆弱。 Numpy使用PyDataMem_NEW/PyDataMem_FREE进行内存管理,这是一个实现细节,这些调用被映射到通常的malloc/free +一些内存跟踪(我不知道它对内存跟踪有什么影响,至少似乎不会崩溃)。

还有更多深奥的案例,其中numpy-library中的free在cython代码中不使用与malloc相同的内存分配器(例如,针对不同的运行时进行链接)。

用于传递/管理数据所有权的正确工具是PyArray_SetBaseObject

首先,我们需要一个python对象,该对象负责释放内存。我在这里使用了一个自制的cdef类(主要是因为日志记录/ demostration),但是显然还有其他可能性:

%%cython
from libc.stdlib cimport free

cdef class MemoryNanny:
    cdef void* ptr # set to NULL by "constructor"
    def __dealloc__(self):
        print("freeing ptr=", <unsigned long long>(self.ptr)) #just for debugging
        free(self.ptr)

    @staticmethod
    cdef create(void* ptr):
        cdef MemoryNanny result = MemoryNanny()
        result.ptr = ptr
        print("nanny for ptr=", <unsigned long long>(result.ptr)) #just for debugging
        return result

 ...

现在,我们将MemoryNanny对象用作内存的前哨对象,当parent-numpy-array被破坏时,该对象将被释放。该代码有点尴尬,因为PyArray_SetBaseObject会窃取引用,而Cython不会自动处理该引用:

%%cython
...
from cpython.object cimport PyObject
from cpython.ref cimport Py_INCREF

cimport numpy as np

#needed to initialize PyArray_API in order to be able to use it
np.import_array()


cdef extern from "numpy/arrayobject.h":
    # a little bit awkward: the reference to obj will be stolen
    # using PyObject*  to signal that Cython cannot handle it automatically
    int PyArray_SetBaseObject(np.ndarray arr, PyObject *obj) except -1 # -1 means there was an error

cdef array_from_ptr(void * ptr, np.npy_intp N, int np_type):
    cdef np.ndarray arr = np.PyArray_SimpleNewFromData(1, &N, np_type, ptr)
    nanny = MemoryNanny.create(ptr)
    Py_INCREF(nanny) # a reference will get stolen, so prepare nanny
    PyArray_SetBaseObject(arr, <PyObject*>nanny) 
    return arr
...

下面是一个示例,说明如何调用此功能:

%%cython
...
from libc.stdlib cimport malloc
def create():
    cdef double *ptr=<double*>malloc(sizeof(double)*8);
    ptr[0]=42.0
    return array_from_ptr(ptr, 8, np.NPY_FLOAT64)

可以如下使用:

>>> m =  create()
nanny for ptr= 94339864945184
>>> m[0]
42.0
>>> del m
freeing ptr= 94339864945184

具有预期的结果/输出。