0%

在Cython中操作数组

Cython提供了很多方法来搭建C/C++内存和Python对象间的桥梁,但是官方的教程只介绍了一些基础的方法。这篇文章就介绍一下我在各个场合学到和用到的Cython封装(多维)数组的技巧。一般而言这个桥梁会分为两部分,Python与Cython和Cython与C/C++。其中Python中的数组主要形式是listarray.arraynumpy.ndarray;Cython中的数组形式有[:,:,:]Memoryview/Buffer)和cython.view.array;C/C++的数组形式有**(指针)、vectorEigen::Vector/Matrix

本篇介绍的主要内容也来自于Cython的文档:Typed Memoryviews

在这里也先介绍一下Cython中的这几个概念: - Memoryview:这是cython提供的一种语法糖,相当于提供了C中int[][][]形式数组的类型。由于Memoryview可以兼容Python的Buffer协议,因此我把他们放在了一起。Memoryview需要指定元素的类型,这个类型必须是内置数值类型或者C结构体。 - cython.view.array:这是Cython提供的一个多维数组类型,与numpy.ndarray非常相似了。 这两个东西也是可以相互转换的,例如

1
2
3
4
5
6
7
8
from cython.view cimport array as cvarray

# Cython array to Memoryview
cyarr = cvarray(shape=(3, 3, 3), itemsize=sizeof(int), format="i")
cdef int [:, :, :] cyarr_view = cyarr

# Memoryview to Cython array
cdef cvarray back = cyarr_view

Python与Cython数组相互转换

Python与Cython之间的转换基本上都由Cython的Memoryview提供了接口,实际上直接赋值就可以。例如官方给出的这段例子:

1
2
3
4
5
6
7
8
9
10
from cpython cimport array as cparray
import numpy as np

# Memoryview on a NumPy array
narr = np.arange(27, dtype=np.dtype("i")).reshape((3, 3, 3))
cdef int [:, :, :] narr_view = narr

# Memoryview on a CPython array
parr = cparray.array('i', range(3))
cdef int [:] parr_view = parr

顺带一提,list对象由于本身不代表一段连续内存,因此需要先转换为arrayndarray再赋值给Memoryview。反过来由于Numpy支持Buffer协议,因此Memoryview和Cython的cython.view.array都可以直接转换为numpy.ndarray,然后转换为arraylist

1
2
3
4
5
6
from cpython cimport array as cparray
import numpy as np

parr = cparray.array('i', range(3))
cdef int [:] parr_view = parr
narr = np.array(parr_view) # explicit version: np.array(parr_view, copy=False)

以上这些代码中的等式都没有发生内存拷贝。

Cython数组与C/C++数组相互转换

Cython的Memoryview同样承担了大量与C/C++数组进行转换的功能,不过Memoryview只支持一种转换方法,就是与raw指针的相互转换:

1
2
3
4
5
6
7
8
from libc.stdlib cimport malloc
cdef double* data = <double*>malloc(sizeof(double) * 4)

# Convert pointer to Memoryview
cdef double[:] view = <double[:2,:2]>data

# Convert Memoryview to pointer
data = &view[0,0]

以上代码的等式中也没有发生内存拷贝。

这里需要指出的是,由于指针本身只是一段内存的代表,因此在转换时制定类型和长度(如<double[4]>),并且需要保证指针指向的数组是C型连续的(多维数组中最后一维的内存是连续的)。如果要将vectorEigen::Matrix转换为Memoryview,那么也同样需要获取其内存指针(vector::dataEigen::Matrix::data)。另外,通过指针转换出来的Memoryview没有引用计数,因此如果你的指针是某个Cython类的成员,那么不要使用指针转换,而使用Buffer协议的方式进行传递。

其他直接转换的方法

除了上面提到的方法之外还有一些直接转换的方法,但是这些方法往往不会做类型和尺寸检查,以及很重要的内存连续性检查(Memoryview会区分C型内存和Fortran型内存),因此使用时需要谨慎。 - cdef vector[int] data; cdef list view = data:Cython提供了list和vector直接转换的接口 - cdef np.ndarray[double] data; cdef double* view = <double*> data.data - cdef np.ndarray[double, ndim=2] data; cdef double* view = &data[0,0] - cdef array.array data; cdef double* view = data.data.as_doubles[0]:利用了Cython中的API

非内置类型的转换

在实际应用过程中还会碰到由复杂元素构成的数组(例如PCL里面的PointXYZ、SLAM里会用到的Quaternion),这时就有将复杂类型(通常是自定义struct)在Python和C/C++之间转换的需求。这时可以选择利用Cython提供的MemoryView,也可以利用Python的Buffer协议直接将C++对象传递给Python。

使用Buffer协议的方法请直接参考Cython文档,使用Memoryview的例子如下:

1
2
3
4
5
6
7
8
9
10
from libc.stdlib cimport malloc
import numpy as np

cdef struct buf:
int size
int count
cdef buf[:] data = <buf[:2]>malloc(sizeof(buf)*2)

print(np.array(data).dtype)
# [('size', '<i4'), ('count', '<i4')]

Treat me some coffee XD