【问题标题】:How to implement Python arrays in a compiled C module using the Python C API如何使用 Python C API 在编译的 C 模块中实现 Python 数组
【发布时间】:2020-01-24 17:39:17
【问题描述】:

我正在尝试尽可能高效地将一个双精度数组从 C 库发送到 Python。 与这个库通信的代码是由不同的公司创建的(它包括许多方法、异常等),但是这个特殊的函数会创建一个列表并从 C 中的每个项目中插入一个 Python 对象数组,如果你关心速度,这是非常低效的。

这是编译后用于创建 python 模块的 C 代码的 sn-p:

static PyObject* foo(PyObject* self, PyObject* args) {

    double *val = 0;
    //more variables

    //Note that this uses the Python C API PyArg_ParseTuple to handle the parameters
    if (! PyArg_ParseTuple(args, "ii", &listID, &size)) {
        //send exception
    }

    //some code here that allocates an array to hold "val" and calls the C library

    PyList_New(size);
    for(i = 0; i < size; i++) {
        PyList_SET_ITEM(retData, i, Py_BuildValue("d", val[i]));
    }

    //free resources, return the Python object
}

我发现Python array 可能很有用,另外还有一个好处是适合多进程。

如果 Python 数组按我想象的那样工作,我可以在 Python 中分配数组,然后 C 库就填充它

from cpython cimport array
import array
from dalibrary import dafunction

cdef array.array a = array.array('d', [])
array.resize(a, 1000)

dafunction(array, 1000)  #  In a very "C" style, the array would be filled with values

print(array)

问题在于我没有找到有关使用 Python 数组所需的 C 代码的文档。至少不使用 Python C API。

注意:我知道 ctypes,但这意味着重写整个模块,如果可能的话,我宁愿不这样做(但缺乏文档可能会驱使我去那里)

似乎有人已经问过类似的问题here,但仍未解决

结果:

我设法执行了我想要的(正如您在其中一个答案中看到的那样),甚至对数组(多进程数组)使用多线程,但令我惊讶的是,它实际上比使用所谓的低效(但健壮的)IPC 方法,例如带有 Python 列表的队列。

由于使用 Python API 很困难,而且它给了我零改进,我认为社区的最佳答案是使用 ctypes 的建议。我会保留我的答案以供参考。也许有人发送大量内存可能会从中受益。

【问题讨论】:

  • 根据您打算如何处理 Python 端的数据,填充列表很容易成为最有效的选择。
  • 数据主要用于从该模块中获取值。在我们的特殊情况下,我们甚至需要将数据传输到不同的 python 进程(长话短说)。列表不能用作共享内存(但直接转换为 C 对象的对象可以)
  • 您也不能共享array.array 数组(尽管使用这样的数组传输数据会更容易)。
  • 我怀疑array.array 数组实际上可能不是您的最佳选择,但无论您最终使用什么,它都可能提供buffer protocol。这包括如果你使用 array.array 数组毕竟。您可以使用缓冲区协议从 C 访问数据。(我没有将其发布为答案,因为我不确定缓冲区形状和步幅如何与各种请求类型交互的一些细节。)
  • @user2357112supportsMonica 我用你关于缓冲协议的评论来构建一个例子。但如果你想添加一个更简洁的例子,我可以接受你的回答。

标签: python c arrays


【解决方案1】:

要将数据从 C 语言转换为 Python 需要做很多事情。首先你应该决定谁来处理内存。是生成数组的 C 代码还是 Python?如果数组在很多地方共享,并且在 Python 不知情的情况下在 C 端被删除,Python 就会崩溃。或相反亦然。

所以复制数组可能不是一个坏主意。

话虽如此,你可以写一个简单的 C 函数

struct Array {
   int size;
   int* data;
}

Array get_my_array() {
    //...
    return {size, val};
}

将其编译为动态库 (my_lib.so) 并使用 Ctypes 包装它(它是一个标准 Python 库,用于访问外部函数)。 您需要描述 Array 返回类型:

from ctypes import Structure, POINTER, c_int, CDLL, find_library, pointer

class Array(Structure):
    __fields__ = [("size", c_int), ("data", POINTER(c_int))]


my_lib = CDLL(find_library("my_lib"))
my_lib.get_my_array.restype = Array

现在您可以获取数组并访问其数据和大小(并手动保护自己免受越界访问)。

例如,您也可以将其传递给 Numpy。幸运的是,这里的答案中有一个相当完整的例子How to create n-dim numpy array from a pointer? 仔细阅读,别忘了清理内存。

请注意,您可以反过来做。如果您在 Python 中知道要创建的数组的大小并且只需要 C 代码来填充它,您可以在 CTypes 中创建它,并将其传递给一个 C 函数,该函数接受指针和大小。

ArrayType = c_int * size
array = ArrayType()

my_lib.populate_array(pointer(array), size)  # left as an exercise

Ctypes 非常方便,当您熟悉 C 语言时,它会很有意义。

【讨论】:

  • 我过去使用过 ctypes。我没有考虑过使用 ctypes,因为这个特定的 Python-module-compiled-from-C-that-uses-a-library 已经使用 Python API 在 C 中编码,并使用低级函数来获取参数类型等(我应该对此有更清楚的了解)。他们甚至在 C 库返回错误时添加了异常生成。所以我希望不要重写整个事情,而只是重写这个特定的功能。
【解决方案2】:

我发现array.array(和Multiprocess.array)确实支持共享内部缓冲区,因为它们都实现了buffer protocol,正如@user2357112-supports-Monica 建议的那样。真正的诀窍在于理解我需要在 PyArg_ParseTuple 中使用“y*”来将参数作为 Py_buffer 导入

这是一个 C 函数示例,它接收数组作为 Py_buffer 并使用它来更改数据。 Python 中两个进程共享同一个缓冲区

C 代码:

//This function receives an object that implements
//the buffer protocol (such as array.array) and the size as an int.
 static PyObject* duplicate(PyObject* self, PyObject* args) {

    int size;  //size could have being calculated with the buffer.len / sizeof(double)
    double *data_ptr;
    Py_buffer buffer;

    //The second parameter indicates that we receive a py_buffer (y*) and and int (i)
    if (! PyArg_ParseTuple(args, "y*i", &buffer, &size)) {
        PyErr_SetString(PyExc_TypeError, "ERROR: Getting expression for method duplicate\n");
        return (PyObject *) NULL;
    }

    data_ptr = (double*) buffer.buf;

    for (int i=0; i < size; i++) {
        data_ptr[i] *= 2.0;
    }

    return Py_BuildValue("");
};

Python 代码:

import testspeed
#import array
import multiprocessing

SIZE = 20

# Parallel processing
def my_func(i, shared_array):
    print(f"Duplicating in process {i}")
    testspeed.duplicate(shared_array, SIZE)

if __name__ == '__main__':
    # Initialize an Array that can be shared between processes
    multi_a = multiprocessing.Array('d', [i+1 for i in range(SIZE)])
    shared_array = multi_a.get_obj()


    print("array in Pyton")
    for i in range(SIZE):
        print(shared_array[i])

    p1=multiprocessing.Process(target=my_func, args=(1, shared_array))
    p2=multiprocessing.Process(target=my_func, args=(2, shared_array))

    p1.start()
    p2.start()
    p1.join()
    p2.join()

    print("\n\narray after executing C code on each process")
    for i in range(SIZE):
        print(shared_array[i])

我仍然不确定我是否在某处遗漏了 PyRelease。但这是我以后可以弄清楚的。

感谢 @daragua 和 @user2357112-supports-Monica 的投入。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-09
    • 2021-10-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多