使用numpy/ctypes暴露C分配的内存缓冲区的更安全方法?

31
我正在为一个使用共享内存缓冲区存储其内部状态的C库编写Python绑定。这些缓冲区的分配和释放是由库本身在Python之外完成的,但是我可以通过从Python中调用包装的构造函数/析构函数来间接控制这个过程。我想将其中一些缓冲区暴露给Python,以便我可以从中读取,并在某些情况下向其中推送值。性能和内存使用是重要考虑因素,因此我希望尽可能避免复制数据。
我的当前方法是创建一个numpy数组,它提供了对ctypes指针的直接视图:
import numpy as np
import ctypes as C

libc = C.CDLL('libc.so.6')

class MyWrapper(object):

    def __init__(self, n=10):
        # buffer allocated by external library
        addr = libc.malloc(C.sizeof(C.c_int) * n)
        self._cbuf = (C.c_int * n).from_address(addr)

    def __del__(self):
        # buffer freed by external library
        libc.free(C.addressof(self._cbuf))
        self._cbuf = None

    @property
    def buffer(self):
        return np.ctypeslib.as_array(self._cbuf)

除了避免复制外,这还意味着我可以使用numpy的索引和分配语法,并直接将其传递给其他numpy函数:
wrap = MyWrapper()
buf = wrap.buffer       # buf is now a writeable view of a C-allocated buffer

buf[:] = np.arange(10)  # this is pretty cool!
buf[::2] += 10

print(wrap.buffer)
# [10  1 12  3 14  5 16  7 18  9]

然而,它也具有固有的危险性:

del wrap                # free the pointer

print(buf)              # this is bad!
# [1852404336 1969367156  538978662  538976288  538976288  538976288
#  1752440867 1763734377 1633820787       8548]

# buf[0] = 99           # uncomment this line if you <3 segfaults

为了更安全,我需要能够检查底层的C指针是否已经被释放,然后再尝试读取/写入数组内容。我有一些关于如何实现这个目标的想法:
  • 一种方法是生成np.ndarray的子类,该子类保存对MyWrapper_cbuf属性的引用,在进行任何读取/写入其底层内存之前检查它是否为None,如果是则抛出异常。
  • 我可以轻松地生成多个视图到同一个缓冲区,例如通过.view转换或切片,因此每个视图都需要继承对_cbuf的引用和执行检查的方法。我认为可以通过重写__array_finalize__来实现这一点,但我不确定具体如何操作。
  • "指针检查"方法还需要在任何读取和/或写入数组内容的操作之前调用。我不了解numpy的内部机制,无法列举要覆盖的所有方法。

我如何实现一个执行此检查的np.ndarray子类?有没有人能提出更好的方法?


更新: 这个类实现了我大部分想要的功能:

class SafeBufferView(np.ndarray):

    def __new__(cls, get_buffer, shape=None, dtype=None):
        obj = np.ctypeslib.as_array(get_buffer(), shape).view(cls)
        if dtype is not None:
            obj.dtype = dtype
        obj._get_buffer = get_buffer
        return obj

    def __array_finalize__(self, obj):
        if obj is None: return
        self._get_buffer = getattr(obj, "_get_buffer", None)

    def __array_prepare__(self, out_arr, context=None):
        if not self._get_buffer(): raise Exception("Dangling pointer!")
        return out_arr

    # this seems very heavy-handed - surely there must be a better way?
    def __getattribute__(self, name):
        if name not in ["__new__", "__array_finalize__", "__array_prepare__",
                        "__getattribute__", "_get_buffer"]:
            if not self._get_buffer(): raise Exception("Dangling pointer!")
        return super(np.ndarray, self).__getattribute__(name)

例如:

wrap = MyWrapper()
sb = SafeBufferView(lambda: wrap._cbuf)
sb[:] = np.arange(10)

print(repr(sb))
# SafeBufferView([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)

print(repr(sb[::2]))
# SafeBufferView([0, 2, 4, 6, 8], dtype=int32)

sbv = sb.view(np.double)
print(repr(sbv))
# SafeBufferView([  2.12199579e-314,   6.36598737e-314,   1.06099790e-313,
#          1.48539705e-313,   1.90979621e-313])

# we have to call the destructor method of `wrap` explicitly - `del wrap` won't
# do anything because `sb` and `sbv` both hold references to `wrap`
wrap.__del__()

print(sb)                # Exception: Dangling pointer!
print(sb + 1)            # Exception: Dangling pointer!
print(sbv)               # Exception: Dangling pointer!
print(np.sum(sb))        # Exception: Dangling pointer!
print(sb.dot(sb))        # Exception: Dangling pointer!

print(np.dot(sb, sb))    # oops...
# -70104698

print(np.extract(np.ones(10), sb))
# array([251019024,     32522, 498870232,     32522,         4,         5,
#               6,         7,        48,         0], dtype=int32)

# np.copyto(sb, np.ones(10, np.int32))    # don't try this at home, kids!

我相信还有其他边缘情况我可能遗漏了。
更新2:我已经尝试使用weakref.proxy,正如@ivan_pozdeev所建议的那样。这是一个好主意,但不幸的是我无法想象它如何与numpy数组一起工作。我可以尝试创建一个对.buffer返回的numpy数组的弱引用:
wrap = MyWrapper()
wr = weakref.proxy(wrap.buffer)
print(wr)
# ReferenceError: weakly-referenced object no longer exists
# <weakproxy at 0x7f6fe715efc8 to NoneType at 0x91a870>

我认为问题在于wrap.buffer返回的np.ndarray实例立即超出作用域。一种解决方法是在类初始化时实例化数组,保持对它的强引用,并使.buffer() getter返回对数组的weakref.proxy

class MyWrapper2(object):

    def __init__(self, n=10):
        # buffer allocated by external library
        addr = libc.malloc(C.sizeof(C.c_int) * n)
        self._cbuf = (C.c_int * n).from_address(addr)
        self._buffer = np.ctypeslib.as_array(self._cbuf)

    def __del__(self):
        # buffer freed by external library
        libc.free(C.addressof(self._cbuf))
        self._cbuf = None
        self._buffer = None

    @property
    def buffer(self):
        return weakref.proxy(self._buffer)

然而,在缓冲区仍被分配时,如果我创建了第二个对同一数组的视图,则会出现问题:
wrap2 = MyWrapper2()
buf = wrap2.buffer
buf[:] = np.arange(10)

buf2 = buf[:]   # create a second view onto the contents of buf

print(repr(buf))
# <weakproxy at 0x7fec3e709b50 to numpy.ndarray at 0x210ac80>
print(repr(buf2))
# array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)

wrap2.__del__()

print(buf2[:])  # this is bad
# [1291716568    32748 1291716568    32748        0        0        0
#         0       48        0] 

print(buf[:])   # WTF?!
# [34525664        0        0        0        0        0        0        0
#         0        0]  

这个问题很严重——在调用wrap2.__del__()之后,我不仅可以读写buf2,它是一个numpy数组视图,对应于wrap2._cbuf,甚至可以读写buf,这是不可能的,因为wrap2.__del__()wrap2._buffer设置为None


3
你考虑过使用Cython编写包装器吗?它提供了更清晰(也可能更安全)的接口,可以通过“类型化内存视图”来获取对内存缓冲区的视图。 - JoshAdel
如果您保留了某个对象的强引用,该对象将为您调用删除器(例如 cffi(您应始终使用而不是 ctypes)具有内置支持 gc 方法的删除器),那么您就不必担心无效的弱引用。 - o11c
@o11c 我还不太确定你的意思。我确实可以对库进行高级控制,因为我可以调用 ctypes 封装的函数,最终会导致缓冲区被分配或释放。虽然实际的分配/释放发生在 Python 之外,但我可以从 Python 中跟踪缓冲区的状态。我想要的是一种方法,确保在我调用库函数释放它们后,无法从这些缓冲区读取或写入数据。我担心的是悬空指针而不是内存泄漏。 - ali_m
1
@ali_m:将_buffer = None赋值并不会释放_buffer,因为其他数组仍然引用它。如果在指针准备好被释放之前手动调用释放指针的函数,那么程序就会出问题。 - user2357112
你能否稍微澄清一下你的问题,去掉现在不相关的部分? - ivan_pozdeev
显示剩余11条评论
6个回答

8

当存在任何numpy数组时,您必须保留对Wrapper的引用。最简单的方法是将此引用保存在ctype缓冲区的属性中:

class MyWrapper(object):
    def __init__(self, n=10):
        # buffer allocated by external library
        self.size = n
        self.addr = libc.malloc(C.sizeof(C.c_int) * n)

    def __del__(self):
        # buffer freed by external library
        libc.free(self.addr)

    @property
    def buffer(self):
        buf = (C.c_int * self.size).from_address(self.addr)
        buf._wrapper = self
        return np.ctypeslib.as_array(buf)

这样,当最后一个引用(例如最后一个numpy数组)被垃圾回收时,您的包装器会自动释放。


5
这是一个由第三方编写的专有库,以二进制形式分发。我可以从C中调用相同的库函数,但这并没有什么帮助,因为我仍然无法访问实际分配和释放缓冲区的代码。例如,我不能自己分配缓冲区,然后将它们作为指针传递给库。
不过,你可以将缓冲区包装在Python扩展类型中。这样,你就可以只公开想要提供的接口,并让扩展类型自动处理缓冲区的释放。这样,Python API就无法进行自由内存读/写操作。
mybuffer.c:
#include <python3.3/Python.h>

// Hardcoded values
// N.B. Most of these are only needed for defining the view in the Python
// buffer protocol
static long external_buffer_size = 32;          // Size of buffer in bytes
static long external_buffer_shape[] = { 32 };   // Number of items for each dimension
static long external_buffer_strides[] = { 1 };  // Size of item for each dimension

//----------------------------------------------------------------------------
// Code to simulate the third-party library
//----------------------------------------------------------------------------

// Allocate a new buffer
static void* external_buffer_allocate()
{
    // Allocate the memory
    void* ptr = malloc(external_buffer_size);

    // Debug
    printf("external_buffer_allocate() = 0x%lx\n", (long) ptr);

    // Fill buffer with a recognizable pattern
    int i;
    for (i = 0; i < external_buffer_size; ++i)
    {
        *((char*) ptr + i) = i;
    }

    // Done
    return ptr;
}

// Free an existing buffer
static void external_buffer_free(void* ptr)
{
    // Debug
    printf("external_buffer_free(0x%lx)\n", (long) ptr);

    // Release the memory
    free(ptr);
}


//----------------------------------------------------------------------------
// Define a new Python instance object for the external buffer
// See: https://docs.python.org/3/extending/newtypes.html
//----------------------------------------------------------------------------

typedef struct
{
    // Python macro to include standard members, like reference count
    PyObject_HEAD

    // Base address of allocated memory
    void* ptr;
} BufferObject;


//----------------------------------------------------------------------------
// Define the instance methods for the new object
//----------------------------------------------------------------------------

// Called when there are no more references to the object
static void BufferObject_dealloc(BufferObject* self)
{
    external_buffer_free(self->ptr);
}

// Called when we want a new view of the buffer, using the buffer protocol
// See: https://docs.python.org/3/c-api/buffer.html
static int BufferObject_getbuffer(BufferObject *self, Py_buffer *view, int flags)
{
    // Set the view info
    view->obj = (PyObject*) self;
    view->buf = self->ptr;                      // Base pointer
    view->len = external_buffer_size;           // Length
    view->readonly = 0;
    view->itemsize = 1;
    view->format = "B";                         // unsigned byte
    view->ndim = 1;
    view->shape = external_buffer_shape;
    view->strides = external_buffer_strides;
    view->suboffsets = NULL;
    view->internal = NULL;

    // We need to increase the reference count of our buffer object here, but
    // Python will automatically decrease it when the view goes out of scope
    Py_INCREF(self);

    // Done
    return 0;
}

//----------------------------------------------------------------------------
// Define the struct required to implement the buffer protocol
//----------------------------------------------------------------------------

static PyBufferProcs BufferObject_as_buffer =
{
    // Create new view
    (getbufferproc) BufferObject_getbuffer,

    // Release an existing view
    (releasebufferproc) 0,
};


//----------------------------------------------------------------------------
// Define a new Python type object for the external buffer
//----------------------------------------------------------------------------

static PyTypeObject BufferType =
{
    PyVarObject_HEAD_INIT(NULL, 0)
    "external buffer",                  /* tp_name */
    sizeof(BufferObject),               /* tp_basicsize */
    0,                                  /* tp_itemsize */
    (destructor) BufferObject_dealloc,  /* tp_dealloc */
    0,                                  /* tp_print */
    0,                                  /* tp_getattr */
    0,                                  /* tp_setattr */
    0,                                  /* tp_reserved */
    0,                                  /* tp_repr */
    0,                                  /* tp_as_number */
    0,                                  /* tp_as_sequence */
    0,                                  /* tp_as_mapping */
    0,                                  /* tp_hash  */
    0,                                  /* tp_call */
    0,                                  /* tp_str */
    0,                                  /* tp_getattro */
    0,                                  /* tp_setattro */
    &BufferObject_as_buffer,            /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT,                 /* tp_flags */
    "External buffer",                  /* tp_doc */
    0,                                  /* tp_traverse */
    0,                                  /* tp_clear */
    0,                                  /* tp_richcompare */
    0,                                  /* tp_weaklistoffset */
    0,                                  /* tp_iter */
    0,                                  /* tp_iternext */
    0,                                  /* tp_methods */
    0,                                  /* tp_members */
    0,                                  /* tp_getset */
    0,                                  /* tp_base */
    0,                                  /* tp_dict */
    0,                                  /* tp_descr_get */
    0,                                  /* tp_descr_set */
    0,                                  /* tp_dictoffset */
    (initproc) 0,                       /* tp_init */
    0,                                  /* tp_alloc */
    0,                                  /* tp_new */
};


//----------------------------------------------------------------------------
// Define a Python function to put in the module which creates a new buffer
//----------------------------------------------------------------------------

static PyObject* mybuffer_create(PyObject *self, PyObject *args)
{
    BufferObject* buf = (BufferObject*)(&BufferType)->tp_alloc(&BufferType, 0);
    buf->ptr = external_buffer_allocate();
    return (PyObject*) buf;
}


//----------------------------------------------------------------------------
// Define the set of all methods which will be exposed in the module
//----------------------------------------------------------------------------

static PyMethodDef mybufferMethods[] =
{
    {"create", mybuffer_create, METH_VARARGS, "Create a buffer"},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};


//----------------------------------------------------------------------------
// Define the module
//----------------------------------------------------------------------------

static PyModuleDef mybuffermodule = {
    PyModuleDef_HEAD_INIT,
    "mybuffer",
    "Example module that creates an extension type.",
    -1,
    mybufferMethods
    //NULL, NULL, NULL, NULL, NULL
};


//----------------------------------------------------------------------------
// Define the module's entry point
//----------------------------------------------------------------------------

PyMODINIT_FUNC PyInit_mybuffer(void)
{
    PyObject* m;

    if (PyType_Ready(&BufferType) < 0)
        return NULL;

    m = PyModule_Create(&mybuffermodule);
    if (m == NULL)
        return NULL;

    return m;
}

test.py

#!/usr/bin/env python3

import numpy as np
import mybuffer

def test():

    print('Create buffer')
    b = mybuffer.create()

    print('Print buffer')
    print(b)

    print('Create memoryview')
    m = memoryview(b)

    print('Print memoryview shape')
    print(m.shape)

    print('Print memoryview format')
    print(m.format)

    print('Create numpy array')
    a = np.asarray(b)

    print('Print numpy array')
    print(repr(a))

    print('Change every other byte in numpy')
    a[::2] += 10

    print('Print numpy array')
    print(repr(a))

    print('Change first byte in memory view')
    m[0] = 42

    print('Print numpy array')
    print(repr(a))

    print('Delete buffer')
    del b

    print('Delete memoryview')
    del m

    print('Delete numpy array - this is the last ref, so should free memory')
    del a

    print('Memory should be free before this line')

if __name__ == '__main__':
    test()

例子

$ gcc -fPIC -shared -o mybuffer.so mybuffer.c -lpython3.3m
$ ./test.py
Create buffer
external_buffer_allocate() = 0x290fae0
Print buffer
<external buffer object at 0x7f7231a2cc60>
Create memoryview
Print memoryview shape
(32,)
Print memoryview format
B
Create numpy array
Print numpy array
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], dtype=uint8)
Change every other byte in numpy
Print numpy array
array([10,  1, 12,  3, 14,  5, 16,  7, 18,  9, 20, 11, 22, 13, 24, 15, 26,
       17, 28, 19, 30, 21, 32, 23, 34, 25, 36, 27, 38, 29, 40, 31], dtype=uint8)
Change first byte in memory view
Print numpy array
array([42,  1, 12,  3, 14,  5, 16,  7, 18,  9, 20, 11, 22, 13, 24, 15, 26,
       17, 28, 19, 30, 21, 32, 23, 34, 25, 36, 27, 38, 29, 40, 31], dtype=uint8)
Delete buffer
Delete memoryview
Delete numpy array - this is the last ref, so should free memory
external_buffer_free(0x290fae0)
Memory should be free before this line

我在编译和运行test.py后遇到了段错误(segfault)的问题,我是在Windows系统上运行的。具体是在"BufferObject *buf = (BufferObject *) (&BufferType)->tp_alloc(&BufferType, 0);"这一行出现了段错误。 - undefined

2

我喜欢@Vikas的方法,但是当我尝试时,我只得到了一个单个FreeOnDel对象的Numpy对象数组。以下方法更简单且有效:

class FreeOnDel(object):
    def __init__(self, data, shape, dtype, readonly=False):
        self.__array_interface__ = {"version": 3,
                                    "typestr": numpy.dtype(dtype).str,
                                    "data": (data, readonly),
                                    "shape": shape}
    def __del__(self):
        data = self.__array_interface__["data"][0]      # integer ptr
        print("do what you want with the data at {}".format(data))

view = numpy.array(FreeOnDel(ptr, shape, dtype), copy=False)

ptr是作为整数的数据的指针(例如ctypesptr.addressof(...))。

这个__array_interface__属性足以告诉Numpy如何将一段内存区域转换为一个数组,然后FreeOnDel对象成为该数组的base。当删除数组时,删除操作会传播到FreeOnDel对象,您可以在该对象中调用libc.free

我甚至可以将这个FreeOnDel类称为“BufferOwner”,因为它的作用就是跟踪所有权。


1

在将其传递给numpy.ctypeslib.as_array方法之前,您只需要使用附加的__del__函数包装器即可。

class FreeOnDel(object):
    def __init__(self, ctypes_ptr):
        # This is not needed if you are dealing with ctypes.POINTER() objects
        # Start of hack for ctypes ARRAY type;
        if not hasattr(ctypes_ptr, 'contents'):
            # For static ctypes arrays, the length and type are stored
            # in the type() rather than object. numpy queries these 
            # properties to find out the shape and type, hence needs to be 
            # copied. I wish type() properties could be automated by 
            # __getattr__ too
            type(self)._length_ = type(ctypes_ptr)._length_
            type(self)._type_ = type(ctypes_ptr)._type_
        # End of hack for ctypes ARRAY type;

        # cannot call self._ctypes_ptr = ctypes_ptr because of recursion
        super(FreeOnDel, self).__setattr__('_ctypes_ptr', ctypes_ptr)

    # numpy.ctypeslib.as_array function sets the __array_interface__
    # on type(ctypes_ptr) which is not called by __getattr__ wrapper
    # Hence this additional wrapper.
    @property
    def __array_interface__(self):
        return self._ctypes_ptr.__array_interface__

    @__array_interface__.setter
    def __array_interface__(self, value):
        self._ctypes_ptr.__array_interface__ = value

    # This is the onlly additional function we need rest all is overhead
    def __del__(self):
        addr = ctypes.addressof(self._ctypes_ptr)
        print("freeing address %x" % addr)
        libc.free(addr)
        # Need to be called on all object members
        # object.__del__(self) does not work
        del self._ctypes_ptr

    def __getattr__(self, attr):
        return getattr(self._ctypes_ptr, attr)

    def __setattr__(self, attr, val):
        setattr(self._ctypes_ptr, attr, val)

测试

In [32]: import ctypes as C

In [33]: n = 10

In [34]: libc = C.CDLL("libc.so.6")

In [35]: addr = libc.malloc(C.sizeof(C.c_int) * n)

In [36]: cbuf = (C.c_int * n).from_address(addr)

In [37]: wrap = FreeOnDel(cbuf)

In [38]: sb = np.ctypeslib.as_array(wrap, (10,))

In [39]: sb[:] = np.arange(10)

In [40]: print(repr(sb))
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)

In [41]: print(repr(sb[::2]))
array([0, 2, 4, 6, 8], dtype=int32)

In [42]: sbv = sb.view(np.double)

In [43]: print(repr(sbv))
array([  2.12199579e-314,   6.36598737e-314,   1.06099790e-313,
         1.48539705e-313,   1.90979621e-313])

In [45]: buf2 = sb[:8]

In [46]: sb[::2] += 10

In [47]: del cbuf   # Memory not freed because this does not have __del__

In [48]: del wrap   # Memory not freed because sb, sbv, buf2 have references

In [49]: del sb     # Memory not freed because sbv, buf have references

In [50]: del buf2   # Memory not freed because sbv has reference

In [51]: del sbv    # Memory freed because no more references
freeing address 2bc6bc0

事实上,一种更简单的解决方案是重写 __del__ 函数。
In [7]: olddel = getattr(cbuf, '__del__', lambda: 0)

In [8]: cbuf.__del__ = lambda self : libc.free(C.addressof(self)), olddel

In [10]: import numpy as np

In [12]: sb = np.ctypeslib.as_array(cbuf, (10,))

In [13]: sb[:] = np.arange(10)

In [14]: print(repr(sb))
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)

In [15]: print(repr(sb))
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)

In [16]: print(repr(sb[::2]))
array([0, 2, 4, 6, 8], dtype=int32)

In [17]: sbv = sb.view(np.double)

In [18]: print(repr(sbv))
array([  2.12199579e-314,   6.36598737e-314,   1.06099790e-313,
         1.48539705e-313,   1.90979621e-313])

In [19]: buf2 = sb[:8]

In [20]: sb[::2] += 10

In [22]: del cbuf   # Memory not freed

In [23]: del sb     # Memory not freed because sbv, buf have references

In [24]: del buf2   # Memory not freed because sbv has reference

In [25]: del sbv    # Memory freed because no more references

1

weakref 是您提出的功能的内置机制。具体而言,weakref.proxy 是一个与所引用对象具有相同接口的对象。在引用对象被处理后,对代理执行的任何操作都会引发 weakref.ReferenceError。您甚至不需要 numpy

In [2]: buffer=(c.c_int*100)()   #acts as an example for an externally allocated buffer
In [3]: voidp=c.addressof(buffer)

In [10]: a=(c.c_int*100).from_address(voidp) # python object accessing the buffer.
                 # Here it's created from raw address value. It's better to use function
                 # prototypes instead for some type safety.
In [14]: ra=weakref.proxy(a)

In [15]: a[1]=1
In [16]: ra[1]
Out[16]: 1

In [17]: del a
In [18]: ra[1]
ReferenceError: weakly-referenced object no longer exists

In [20]: buffer[1]
Out[20]: 1

正如您所见,在任何情况下,您都需要一个普通的Python对象来覆盖C缓冲区。如果外部库拥有内存,则必须在C级别释放缓冲区之前删除该对象。如果您自己拥有内存,则只需按照常规方式创建ctypes对象,然后在删除时它将被释放。

因此,如果您的外部库拥有内存并且可以随时释放(您的规范对此含糊不清),则必须以某种方式告知您即将执行此操作-否则,您无法知道需要采取必要的措施。


谢谢您的建议。不幸的是,我认为weakref.proxy无法正确处理多个numpy数组由同一缓冲区支持的情况(请参见我的更新)。 - ali_m
我已经对问题进行了一些澄清。尽管缓冲区的分配/释放发生在 Python 之外,但通过调用包装的构造函数/析构函数,我可以间接地控制这种情况发生的时机。 - ali_m
1
如果我只需要一个容器,那么 ctypes 数组就可以了,但实际上我想要使用缓冲区的内容进行矢量化计算。我认为 @Daniel 的方法可能足够了,因为视图和切片将通过它们的 .base 属性保留对它们派生自哪个父数组的引用。只要从指针创建的初始数组持有对其父对象的引用,那么就足以确保在所有其他视图都消失之前不会删除父对象。 - ali_m
@ali_m 但是你告诉我们缓冲区被库“从下面”对象删除 - 因此它必须有一种积极的方式来向所有相关人员发出信号,表明它已经失效,无论有多少Python引用与之相关。 - ivan_pozdeev
如果一个缓冲区对象在存在任何 Python 引用时无法被释放,那么弱引用确实不需要使用。它们的作用在于需要在不考虑是否存在引用的情况下处理对象的场景。 Translated text: 如果一个缓冲区对象在存在任何 Python 引用时无法被释放,那么弱引用确实不需要使用。它们的作用在于需要在不考虑是否存在引用的情况下处理对象的场景。 - ivan_pozdeev
显示剩余3条评论

0

如果您可以完全从Python控制C缓冲区的生命周期,那么您实际上拥有一个Python“缓冲区”对象,ndarray应该使用它。

因此,

  • 有两种基本连接它们的方法:
    • 缓冲区 -> ndarray
    • ndarray -> 缓冲区
  • 还有一个问题是如何实现缓冲区本身

缓冲区 -> ndarray

不安全:没有任何自动引用buffer来维持ndarray的生命周期。引入第三个对象来保存两者的引用也不会更好:这样您只需跟踪第三个对象而不是buffer

ndarray -> 缓冲区

“现在你说对了!”由于手头的任务是“ndarray应该使用的缓冲区”,所以这是自然的选择。

事实上,numpy 内置了一个机制:任何不拥有自己内存的 ndarray 都会在其 base 属性中保存对拥有内存的对象的引用(从而防止后者被垃圾回收)。对于视图,该属性会自动相应地分配(如果其 baseNone,则分配给父对象,否则分配给父对象的 base)。
问题在于,你不能随便放置任何旧对象。相反,该属性由构造函数填充,并首先通过其审查建议的对象。
因此,如果我们只能构造一些自定义对象,numpy.array 就可以接受并考虑重用内存(numpy.ctypeslib.as_array 实际上是 numpy.array(copy=False) 的包装器,带有一些健全性检查)...

<...>


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