前言
之所以要将ndarray(numpy的通用数据类型)转为C++的原生数组,或者说在Python中调用C++编译好的二级制文件中的函数这个事的核心原因在于Python作为一门解释型语言 ,最大的诟病便是它的运行速度过于慢,最典型的例子就是循环。
Python的执行循环的速度远远慢于C++这样的编译型语言,具体原因在这里不过多解释。基于编译形语言的这个优点,我在做科学计算的时候,涉及到一些需要用到自主编写的计算量较为庞大的算法时,通常会选择采用C++编写,但是由于Python做数据分析和科学计算的便利性,我的主程序框架又通常都是基于Python开发的,所以需要在Python的调用C++编写好的函数以提高运算效率。
具体怎么使用pybind11在Python代码中调用编译好的C++二级制文件 请参考我的另一篇博客:pybind11:实现Python调用C++代码(入门)
调用函数,离不开参数(C++的各种数据结构),做数据分析和计算离不开numpy,于是就需要解决一个核心问题:ndarray转C++原生数组,然后通过C++程序计算后将计算结果(C++数组)再转为 ndarray,实现C++与Python的无缝衔接。
阅读官方文档
官方文档永远是最好的学习材料,找到pybind官方文档的网址:https://pybind11.readthedocs.io/en/stable/index.html
这个文档对于C++和Python的数据类型的转化以及底层原理写的都比较详细,对这方面感兴趣的朋友可慢慢参考,我主要介绍其中的核心方法来实现我的目标问题。
在目录那一栏找到 Python C++ interface 中的 NumPy,
里面主要介绍了 Numpy 如何与C++联动,我选择 Vectorizing functions 中的一个比较好的例子展开说明:
简单读一下这个代码,
这是一个C++代码 ,编写了一个返回值类型和接受参数类型都为py::array_t<double> 的函数,py::array_t<double>可以理解为在numpy.h 中的定义一个类模板,用于在C++代码中表示NumPy数组(即ndarray)的数据结构。
函数内部使用了py::buffer_info 来获取输入数组的信息(request ),包括维度(ndim )、形状(shape )、大小(size)等。然后进行了一些简单的错误检查,确保输入数组是一维的,且形状匹配。
接下来,函数创建了一个新的 py::array_t<double> 对象 result,它的大小与输入数组 input1 的大小(即buf1.size)相同。这个数组用于存储相加后的结果。
在对result进行request获取缓冲区信息 后,将缓冲区指针转换 为double*类型,并分别赋值给ptr1、ptr2和ptr3指针,指向输入数组和结果数组的内存位置。
接下来,使用一个循环遍历数组的每个元素(根据 buf1.shape[0] 的值),将相应位置上的input1和input2数组的元素相加,并将结果存储到result数组中。
最后,返回result数组,即相加后的结果。
利用PYBIND11_MODULE这个宏把 add_arrays 这个函数绑定到C++函数。
py::buffer_info (装载ndarray的信息的数据类型)的属性主要有:
最常用的是ndim(维度)shape(形状)
采用这个代码made一个项目(记得更改模板名为你自己配置的模板名(PYBIND11_MODULE的第一个参数)),配置生成后,老套路,将生成的 pyd 文件拖放至与python脚本一个目录下便能使用add_arrays这个函数,实例如下(我的模板名叫 tryPybind):
python
import numpy as np
import tryPybind
a = np.array([1, 2, 3])
b = np.array([3, 4, 5])
c = np.array([[1, 2, 3], [4, 5, 6]])
d = np.array([1, 2, 3, 4])
e = tryPybind.add_arrays(a, b)
# f = tryPybind.add_arrays(a, c) # RuntimeError: Number of dimensions must be one
# g = tryPybind.add_arrays(a, d) # RuntimeError: Input shapes must match
print(e) # [4. 6. 8.]
如上便演示如何用C++代码计算ndarray,仔细观察便会发现,这个代码能实现的功能极少,且并其实并未将ndarray转为C++原生数组,只是在C++代码中基于ndarray进行运算,这样便不能套用编写好的C++算法,当然,这个代码仍然具有比较大的启发作用。下面我将介绍我自主编写的ndarray转C++原生数组的算法,具有很强的拓展性。
C++数组 --> ndarray
由于C++数组的数据结构比较简单,转为ndarray比较容易,只需两步:
- 创建输出数组(ndarray)
- 将C++数组的数据拷贝到输出数组(ndarray)
直接上代码(我只定义了一维和二维的情况,想要更高维度的代码类似)用到了C++的模板函数和函数重写:
cpp
template<typename T>
py::array_t<T> CToNdarray(T* Array, int len) {
// 创建输出数组
py::array_t<T> outputArray(len);
auto outputArrayInfo = outputArray.request();
T* outputPtr = static_cast<T*>(outputArrayInfo.ptr);
// 将结果拷贝到输出数组
for (int i = 0; i < len; ++i) {
outputPtr[i] = Array[i];
}
return outputArray;
}
template<typename T>
py::array_t<T> CToNdarray(T** Array, int rows, int cols) {
// 创建输出数组
py::array_t<T> outputArray({rows, cols});
auto outputArrayInfo = outputArray.request();
T* outputPtr = static_cast<T*>(outputArrayInfo.ptr);
// 将结果拷贝到输出数组
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
outputPtr[i * cols + j] = Array[i][j];
}
}
return outputArray;
}
ndarray --> C++数组
ndarray是一种较为复杂的数据结构,具有很多属性,而C++的数组又是一种比较简单的数据结构,如果像C++转ndarray一样只拷贝数据而不做其他处理的话就会损失很多信息像形状,维度等,这些是从C++数组出发所无法计算得到的。
一个以C++数组为参数的算法往往需要数组的长度为参数,所以综合考虑,以类的形式储存C++数组(数组,形状,维度都以属性的形式存在于一个对象中)代码如下(同样只定义了一维和二维的情况):
cpp
// 将ndarray转化为C++数组
template <typename T = double>
class NdarrayToCppArray {
public:
// 储存维度
int dim;
// 储存每个维度下的长度
int* lens;
// 存储一维向量
T* Vector;
// 存储二维矩阵
T** Matrix;
NdarrayToCppArray(py::array_t<T>& inputNdarray){
// 计算维度
this->dim = inputNdarray.ndim();
// 处理输入的一维ndarray
auto inputNdarrayInfo = inputNdarray.request();
T* inputNdarrayDataPtr = static_cast<T*>(inputNdarrayInfo.ptr);
// 计算每个维度下的长度
this->lens = new int[this->dim];
for (int i = 0; i < this->dim; i++){
this->lens[i] = inputNdarrayInfo.shape[i];
}
if(this->dim == 1){ // 如果是一维的ndarray
// 将输入数据转换为一维数组
this->Vector = new T[this->lens[0]];
for (int i = 0; i < this->lens[0]; ++i) {
this->Vector[i] = inputNdarrayDataPtr[i];
}
// 矩阵则赋为空指针
this->Matrix = nullptr;
}
else if(this->dim == 2) { // 如果是二维的ndarray
// 将输入的数据转化为二维数组
this->Matrix = new T*[this->lens[0]];
for (int i = 0; i < this->lens[0]; ++i) {
this->Matrix[i] = new T[this->lens[1]];
for (int j = 0; j < this->lens[1]; ++j){
this->Matrix[i][j] = inputNdarrayDataPtr[i * this->lens[1] + j];
}
}
// 向量则赋为空指针
this->Vector = nullptr;
}
}
};
由于这几个算法目前基本能满足我的要求,我并未特别优化这些代码,如果朋友们有想法和建议,欢迎私信交流。
测试
将上面两个算法放置在工具cpp文件中,需要用到直接调用即可,下面来测试算法的执行结构:输出维度 ,输出形状 ,输出数组
cpp
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
#include "pybind11_tools.cpp"
namespace py = pybind11;
// 测试输出数组
py::array_t<double> Ndarray(py::array_t<double>& inputArray){
NdarrayToCppArray<double> InputArray(inputArray);
int dim = InputArray.dim;
py::array_t<int> outputArray;
if (dim == 1){
double* result = InputArray.Vector;
int len = InputArray.lens[0];
outputArray = CToNdarray(result, len);
}
else if (dim == 2){
double** result = InputArray.Matrix;
int row = InputArray.lens[0];
int col = InputArray.lens[1];
outputArray = CToNdarray(result, row, col);
}
return outputArray;
}
// 测试输出维度
int Dim(py::array_t<double>& inputVector){
NdarrayToCppArray<double> InputVertor(inputVector);
return InputVertor.dim;
}
// 测试输出形状
py::array_t<int> Shape(py::array_t<double>& inputVector){
NdarrayToCppArray<double> InputVertor(inputVector);
int len = InputVertor.dim;
int* Length = InputVertor.lens;
py::array_t<int> outputArray = CToNdarray(Length, len);
return outputArray;
}
PYBIND11_MODULE(tryPybind, m) {
m.def("ndarray", &Ndarray);
m.def("ndim", &Dim);
m.def("shape", &Shape);
}
之所以没有定义模板是因为在pybind11中,模板函数无法直接导出为Python可调用的函数,所以需要用到什么类型就定义什么类型,由于numpy的浮点数类型默认是np.float64 ,所以便定义双精度double数组来装载ndarray。配置生成后,编写Python脚本:
cpp
import numpy as np
import tryPybind
a = np.random.rand(4, 3)
print('调用python代码')
print('维度: ' + str(a.ndim))
print('形状: ' + str(a.shape))
print('数组: ' + str(a))
print('类型: ' + str(type(a)))
print('调用C++代码')
print('维度: ' + str(tryPybind.ndim(a)))
print('形状: ' + str(tryPybind.shape(a)))
print('数组: ' + str(tryPybind.ndarray(a)))
print('类型: ' + str(type(tryPybind.ndarray(a))))
输出结果:
这样便实现了先把一个ndarray转为一个C++原生数组,进行运算,再转为一个ndarray,进行输出,如果需要C++做矩阵运算,只需在转为C++数组后加入算法即可,具有很强的拓展性,并且观察python脚本后不难发现,调用C++和Python的代码可以说是无缝衔接!
补充 :像 py::array_t a(py::array_t input1, py::array_t input2) 这样类型结构的函数,返回值和参数都是ndarray,而事实上,传参除了可以传入ndarray外,还可以传入Python的列表(list)元组(tuple),但是返回值还是ndarray,那如果需要返回值为 list 或者 tuple的话,可以考虑使用 std::vector 和 std::tuple 这两种数据结构(二者都是 C++ 标准库中的一个容器类模板,可以存储数据)重新定义转换函数,这样的话非常麻烦且难管理,既然C++和Python的代码可以无缝衔接,那不如直接使用Python原生代码 list() tuple() 对返回的ndarray直接转化为你需要的数据结构来的方便。
以下这篇博客一个实际的例子说明该项技术(ndarray和C++数组的相互转换)所带来的拓展性和便利性,以及在Python中调用C++代码所带来的巨大优势:
pybind11:对比C++和Python解线性方程组的速度