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

2023-12-17 10:52

本文主要是介绍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

mulu

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

lizi

简单读一下这个代码,

这是一个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的信息的数据类型)的属性主要有:

shux
最常用的是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 matchprint(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();// 处理输入的一维ndarrayauto 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 <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脚本:

import numpy as np
import tryPybinda = 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))))

输出结果:

shuchu

这样便实现了先把一个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解线性方程组的速度

这篇关于pybind11:实现ndarray转C++原生数组的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/504117

相关文章

SpringBoot3实现Gzip压缩优化的技术指南

《SpringBoot3实现Gzip压缩优化的技术指南》随着Web应用的用户量和数据量增加,网络带宽和页面加载速度逐渐成为瓶颈,为了减少数据传输量,提高用户体验,我们可以使用Gzip压缩HTTP响应,... 目录1、简述2、配置2.1 添加依赖2.2 配置 Gzip 压缩3、服务端应用4、前端应用4.1 N

SpringBoot实现数据库读写分离的3种方法小结

《SpringBoot实现数据库读写分离的3种方法小结》为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式,在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三... 目录一、数据库读写分离概述二、方案一:基于AbstractRoutingDataSource实现动态

Python FastAPI+Celery+RabbitMQ实现分布式图片水印处理系统

《PythonFastAPI+Celery+RabbitMQ实现分布式图片水印处理系统》这篇文章主要为大家详细介绍了PythonFastAPI如何结合Celery以及RabbitMQ实现简单的分布式... 实现思路FastAPI 服务器Celery 任务队列RabbitMQ 作为消息代理定时任务处理完整

Java枚举类实现Key-Value映射的多种实现方式

《Java枚举类实现Key-Value映射的多种实现方式》在Java开发中,枚举(Enum)是一种特殊的类,本文将详细介绍Java枚举类实现key-value映射的多种方式,有需要的小伙伴可以根据需要... 目录前言一、基础实现方式1.1 为枚举添加属性和构造方法二、http://www.cppcns.co

使用Python实现快速搭建本地HTTP服务器

《使用Python实现快速搭建本地HTTP服务器》:本文主要介绍如何使用Python快速搭建本地HTTP服务器,轻松实现一键HTTP文件共享,同时结合二维码技术,让访问更简单,感兴趣的小伙伴可以了... 目录1. 概述2. 快速搭建 HTTP 文件共享服务2.1 核心思路2.2 代码实现2.3 代码解读3.

MySQL双主搭建+keepalived高可用的实现

《MySQL双主搭建+keepalived高可用的实现》本文主要介绍了MySQL双主搭建+keepalived高可用的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录一、测试环境准备二、主从搭建1.创建复制用户2.创建复制关系3.开启复制,确认复制是否成功4.同

Java实现文件图片的预览和下载功能

《Java实现文件图片的预览和下载功能》这篇文章主要为大家详细介绍了如何使用Java实现文件图片的预览和下载功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... Java实现文件(图片)的预览和下载 @ApiOperation("访问文件") @GetMapping("

使用Sentinel自定义返回和实现区分来源方式

《使用Sentinel自定义返回和实现区分来源方式》:本文主要介绍使用Sentinel自定义返回和实现区分来源方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Sentinel自定义返回和实现区分来源1. 自定义错误返回2. 实现区分来源总结Sentinel自定

Java实现时间与字符串互相转换详解

《Java实现时间与字符串互相转换详解》这篇文章主要为大家详细介绍了Java中实现时间与字符串互相转换的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、日期格式化为字符串(一)使用预定义格式(二)自定义格式二、字符串解析为日期(一)解析ISO格式字符串(二)解析自定义

opencv图像处理之指纹验证的实现

《opencv图像处理之指纹验证的实现》本文主要介绍了opencv图像处理之指纹验证的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录一、简介二、具体案例实现1. 图像显示函数2. 指纹验证函数3. 主函数4、运行结果三、总结一、