在 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')]