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

相关文章

分布式锁在Spring Boot应用中的实现过程

《分布式锁在SpringBoot应用中的实现过程》文章介绍在SpringBoot中通过自定义Lock注解、LockAspect切面和RedisLockUtils工具类实现分布式锁,确保多实例并发操作... 目录Lock注解LockASPect切面RedisLockUtils工具类总结在现代微服务架构中,分布

Java使用Thumbnailator库实现图片处理与压缩功能

《Java使用Thumbnailator库实现图片处理与压缩功能》Thumbnailator是高性能Java图像处理库,支持缩放、旋转、水印添加、裁剪及格式转换,提供易用API和性能优化,适合Web应... 目录1. 图片处理库Thumbnailator介绍2. 基本和指定大小图片缩放功能2.1 图片缩放的

Python使用Tenacity一行代码实现自动重试详解

《Python使用Tenacity一行代码实现自动重试详解》tenacity是一个专为Python设计的通用重试库,它的核心理念就是用简单、清晰的方式,为任何可能失败的操作添加重试能力,下面我们就来看... 目录一切始于一个简单的 API 调用Tenacity 入门:一行代码实现优雅重试精细控制:让重试按我

Redis客户端连接机制的实现方案

《Redis客户端连接机制的实现方案》本文主要介绍了Redis客户端连接机制的实现方案,包括事件驱动模型、非阻塞I/O处理、连接池应用及配置优化,具有一定的参考价值,感兴趣的可以了解一下... 目录1. Redis连接模型概述2. 连接建立过程详解2.1 连php接初始化流程2.2 关键配置参数3. 最大连

Python实现网格交易策略的过程

《Python实现网格交易策略的过程》本文讲解Python网格交易策略,利用ccxt获取加密货币数据及backtrader回测,通过设定网格节点,低买高卖获利,适合震荡行情,下面跟我一起看看我们的第一... 网格交易是一种经典的量化交易策略,其核心思想是在价格上下预设多个“网格”,当价格触发特定网格时执行买

python设置环境变量路径实现过程

《python设置环境变量路径实现过程》本文介绍设置Python路径的多种方法:临时设置(Windows用`set`,Linux/macOS用`export`)、永久设置(系统属性或shell配置文件... 目录设置python路径的方法临时设置环境变量(适用于当前会话)永久设置环境变量(Windows系统

C++11范围for初始化列表auto decltype详解

《C++11范围for初始化列表autodecltype详解》C++11引入auto类型推导、decltype类型推断、统一列表初始化、范围for循环及智能指针,提升代码简洁性、类型安全与资源管理效... 目录C++11新特性1. 自动类型推导auto1.1 基本语法2. decltype3. 列表初始化3

C++11右值引用与Lambda表达式的使用

《C++11右值引用与Lambda表达式的使用》C++11引入右值引用,实现移动语义提升性能,支持资源转移与完美转发;同时引入Lambda表达式,简化匿名函数定义,通过捕获列表和参数列表灵活处理变量... 目录C++11新特性右值引用和移动语义左值 / 右值常见的左值和右值移动语义移动构造函数移动复制运算符

Python对接支付宝支付之使用AliPay实现的详细操作指南

《Python对接支付宝支付之使用AliPay实现的详细操作指南》支付宝没有提供PythonSDK,但是强大的github就有提供python-alipay-sdk,封装里很多复杂操作,使用这个我们就... 目录一、引言二、准备工作2.1 支付宝开放平台入驻与应用创建2.2 密钥生成与配置2.3 安装ali

Spring Security 单点登录与自动登录机制的实现原理

《SpringSecurity单点登录与自动登录机制的实现原理》本文探讨SpringSecurity实现单点登录(SSO)与自动登录机制,涵盖JWT跨系统认证、RememberMe持久化Token... 目录一、核心概念解析1.1 单点登录(SSO)1.2 自动登录(Remember Me)二、代码分析三、