Force NumPy ndarray возьмет на себя ответственность за свою память в Cython
После этот ответ на вопрос "Могу ли я заставить numpy ndarray взять на себя ответственность за свою память?" Я попытался использовать функцию API Python C PyArray_ENABLEFLAGS
через Cython Обертка NumPy и обнаружила, что она не отображается.
Следующая попытка разоблачить его вручную (это всего лишь минимальный пример, воспроизводящий сбой)
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?
Ответы
Ответ 1
У вас просто небольшие ошибки в определении интерфейса. Для меня работали следующие:
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.
Ответ 2
Решение @Stefan работает для большинства сценариев, но несколько хрупко. Numpy использует PyDataMem_NEW/PyDataMem_FREE
для управления памятью, и это деталь реализации, что эти вызовы отображаются на обычную malloc/free
+ некоторую трассировку памяти (я не знаю, какая влияние решения Stefan на трассировку памяти, по крайней мере, кажется, что оно не падает).
Также возможны и более эзотерические случаи, когда free
из numpy-library не использует тот же распределитель памяти, что и malloc
в коде Cython (связан с разными временами выполнения, например, как в этом github- вопрос).
Правильный инструмент для передачи/управления владением данными - PyArray_SetBaseObject
.
Сначала нам нужен питон -object, который отвечает за освобождение памяти. Я использую самодельный класс cdef (в основном из-за регистрации/демострации), но, очевидно, есть и другие возможности:
%%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
-object в качестве сторожа для памяти, которая освобождается сразу после уничтожения родительского массива. Код немного неловкий, потому что 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.flags
...
OWNDATA : False
...
>>> m[0]
42.0
>>> del m
freeing ptr= 94339864945184
с результатами/выводом, как и ожидалось.
Примечание: полученные массивы на самом деле не владеют данными (т.е. флаги возвращают OWNDATA : False
), потому что память принадлежит няне памяти, но результат тот же: память освобождается, как только массив становится удалено (потому что никто больше не имеет ссылки на няню).