pybind11:实现ndarray转C++原生数组

前言

之所以要将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

pybind11:实现ndarray转C++原生数组_第1张图片

里面主要介绍了 Numpy 如何与C++联动,我选择 Vectorizing functions 中的一个比较好的例子展开说明:

pybind11:实现ndarray转C++原生数组_第2张图片

简单读一下这个代码,

这是一个C++代码,编写了一个返回值类型和接受参数类型都为py::array_t 的函数,py::array_t可以理解为在numpy.h 中的定义一个类模板,用于在C++代码中表示NumPy数组(即ndarray)的数据结构。

函数内部使用了py::buffer_info来获取输入数组的信息(request),包括维度(ndim)、形状(shape)、大小(size)等。然后进行了一些简单的错误检查,确保输入数组是一维的,且形状匹配。

接下来,函数创建了一个新的 py::array_t 对象 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的信息的数据类型)的属性主要有:

pybind11:实现ndarray转C++原生数组_第3张图片
最常用的是ndim(维度)shape(形状)

采用这个代码made一个项目(记得更改模板名为你自己配置的模板名(PYBIND11_MODULE的第一个参数)),配置生成后,老套路,将生成的 pyd 文件拖放至与python脚本一个目录下便能使用add_arrays这个函数,实例如下(我的模板名叫 tryPybind):

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比较容易,只需两步:

  1. 创建输出数组(ndarray)
  2. 将C++数组的数据拷贝到输出数组(ndarray)

直接上代码(我只定义了一维和二维的情况,想要更高维度的代码类似)用到了C++的模板函数和函数重写:

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++数组(数组,形状,维度都以属性的形式存在于一个对象中)代码如下(同样只定义了一维和二维的情况):

// 将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文件中,需要用到直接调用即可,下面来测试算法的执行结构:输出维度输出形状输出数组

#include 
#include 
#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脚本:

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))))

输出结果:

pybind11:实现ndarray转C++原生数组_第4张图片

这样便实现了先把一个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::vectorstd::tuple 这两种数据结构(二者都是 C++ 标准库中的一个容器类模板,可以存储数据)重新定义转换函数,这样的话非常麻烦且难管理,既然C++和Python的代码可以无缝衔接,那不如直接使用Python原生代码 list() tuple() 对返回的ndarray直接转化为你需要的数据结构来的方便。

以下这篇博客一个实际的例子说明该项技术(ndarray和C++数组的相互转换)所带来的拓展性和便利性,以及在Python中调用C++代码所带来的巨大优势:
pybind11:对比C++和Python解线性方程组的速度

你可能感兴趣的:(python,c++)