使用Cython在Python和C++间互传大小事先未知的numpy数组
dev/python | dev/c++ | dev/cython
从C++传到Python
常见的教程如这个问题及回答是将大小已知的numpy数组传入传出C++,如确定会从C++传出大小为$M \times N$的矩阵。方法简单讲就是在Python端分配一个大小为$M \times N$的矩阵,把指向这个矩阵的指针传给C++,C++负责修改矩阵的内容,结束后矩阵就自动“传回”了。
然而有时我们事先不知道从C++传回的矩阵是多大,这时我们可以用这个回答所提及的技术,即从C++传回std::vector,然后在Python端把它无拷贝地转成numpy数组。
例子:从C++传回$M \times 2$大小的矩阵,$M$在Python端未知。例子主要来源于网络,但我稍微换了一下应用,并修改了里面的谬误。
doit.h:
#ifndef _DOIT_H_
#define _DOIT_H_
#include <vector>
std::vector<long> arange2d();
#endif
doit.cpp:
#include "doit.h"
std::vector<long> arange2d() {
std::vector<long> arr(10);
long x = 0;
for (auto i = arr.begin(); i != arr.end(); ++i) {
*i = x++;
}
return arr;
}
fast.pyx:
from libcpp.vector cimport vector
cdef extern from 'doit.h':
vector[long] arange2d()
cdef class ArrayWrapper:
cdef vector[long] v
cdef Py_ssize_t shape[2];
cdef Py_ssize_t strides[2];
def set_data(self, vector[long]& data):
self.v.swap(data) # 注(1)
def __getbuffer__(self, Py_buffer *buf, int flags):
self.shape[0] = self.v.size() // 2
self.shape[1] = 2
self.strides[0] = self.shape[1] * sizeof(long)
self.strides[1] = sizeof(long)
# 注(2)
buf.buf = <char *> self.v.data()
buf.format = 'l' # 注(3)
buf.internal = NULL
buf.itemsize = <Py_ssize_t> sizeof(long)
buf.len = self.v.size() * sizeof(long)
buf.ndim = 2
buf.obj = self
buf.readonly = 0
buf.shape = self.shape
buf.strides = self.strides
buf.suboffsets = NULL
def pyarange2d():
cdef vector[long] arr = arange2d()
cdef ArrayWrapper wrapper = ArrayWrapper()
wrapper.set_data(arr)
return np.asarray(wrapper)
- 注(1):
std::vector<T>::swap完成了无拷贝传值,另一种方法是用std::move,不过那需要cdef extern from '<utility>' namespace 'std' nogil: vector[long] move(vector[long]),应该是这样,不过我没试过 - 注(2):numpy的Buffer Protocol见此处,里面讲了
buf需要设置哪些属性 - 注(3):
buf.format如何设置见此处
至于从C++传回Python的多维数组有两个及以上的维度不知道的话(已知维度总数ndim),网络上没找到答案,但我是这么做的:
- 传给C++一个指向
Py_ssize_t类型、长度为ndim的数组(即待传回数组的shape)的指针 - C++传回一个
std::vector并修改shape元素为合适的值 - 按照
shape及std::vector的元素类型填写buf的属性,完成std::vector到numpy数组的转换
从Python传到C++
这应该已经耳熟能详了,我就不在此赘述了。不过有一点需要注意。传double数组时没问题,各平台double都对应numpy.float64。传int数组时需注意,Windows下对应numpy.int32、Linux/Mac下对应numpy.int64。所以直接用传double数组的方法传int数组会报这个错:
Cannot assign type 'int_t *' to 'int *'
见这个问题(就是我提的)。目前我还没有优雅的解决方法。我笨拙的方法(受ead的启发)(请对照着“这个问题”看)如下:把所有的int全替换为int64_t(或int32_t,一致就行),例如int * => int64_t *、np.int_t => np.int64_t,然后在dotit.h包含头文件的地方加上#include <cstdint>,在q.pyx头部加上from libc.stdint cimport int64_t。应该就可以编译了。
补充一点我近期观察到的:以上workaround在Windows下(Visual Studio 2022)貌似不行,会报不能将numpy的int32_t转为int32_t,类似这样的错。在Darwin和Linux下都是能通过编译的。