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

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

hdu2241(二分+合并数组)

题意:判断是否存在a+b+c = x,a,b,c分别属于集合A,B,C 如果用暴力会超时,所以这里用到了数组合并,将b,c数组合并成d,d数组存的是b,c数组元素的和,然后对d数组进行二分就可以了 代码如下(附注释): #include<iostream>#include<algorithm>#include<cstring>#include<stack>#include<que

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名