首页 > 编程语言 >Pybind11:使用C++编写Python模块

Pybind11:使用C++编写Python模块

时间:2023-08-23 11:23:29浏览次数:57  
标签:__ Python self py C++ Vector Pybind11 def

放假摆了一周了。看论文实在不是什么有意思的活。

这两天研究了一下Pybind11的用法。使用C/C++和Python混合编程的想法很早就有了,在大一的一次比赛时曾经实践过(虽然不是我写的),当时获得了比较显著的性能提升。但是当时用的是Swig,据队友说Swig对于NumPy的支持极为阴间,当时调试花了好几天的时间。在混合编程中NumPy的传递极为重要,因为混合编程的主要使用场景就在于python不擅长进行大规模的数值计算,以及不擅长进行并行,而python科学计算的核心就在于NumPy。

相比之下,Pybind11比Swig更新,而且对于NumPy有专门的支持。它是一组C++的头文件,功能类似于Boost.Python,但更加轻量化。

基本使用

代码

找到两篇很详细的Blog,一篇是知乎上的,一篇是一个个人博客

基本原理是使用C++编译器将cpp模块生成动态库(.so/.pyd),python能够直接识别动态库为模块导入进行使用。

C++模块正常编写,包括函数或者类。在最后需要加上PYBIND11_MODULE进行绑定。能够绑定的对象包括:函数、类(包括重载、继承、操作符重载、虚函数等)。C++操作符重载一部分直接对应于Python的操作符,还有一部分对应于Python的魔术方法。转换成魔术方法时,需要由C++侧去匹配魔术方法的参数表。参数表中的self对应于一个该类型的引用。

其他的详细步骤我不写了,上面两篇博客写得很详细了。放一个我自己写的例子。这里有一部分代码是用CodeGeeX写的,不得不承认我写代码没有它快qwq

#include "pybind11/detail/common.h"
#include <pybind11/pybind11.h>
#include <pybind11/operators.h>
#include <omp.h>
#include <iostream>
#include <string>

template<class T>
class Vector {

public:

    Vector(int size) {
        this->size = size;
        v = new T[size];
        for (int i = 0; i < size; ++i) {
            v[i] = 0;
        }
    }

    ~Vector() {
        delete[] v;
    }

    T& operator[](int i) {
        return v[i];
    }

    const Vector<T>& operator+=(const Vector<T>& other) {
        if (size != other.size) {
            throw "Vector sizes do not match";
        }
        #pragma omp parallel for
        for (int i = 0; i < size; ++i) {
            v[i] += other.v[i];
        }
        return *this;
    }

    void print() const{
        for (int i = 0; i < size; ++i) {
            std::cout << v[i] << " ";
        }
        std::cout << std::endl;
    }

    const std::string toString() const{
        std::string s;
        for (int i = 0; i < size; ++i) {
            s += std::to_string(v[i]) + " ";
        }
        return s;
    }

private:

    T* v;
    int size;

};

/* do binding */
PYBIND11_MODULE(vector, obj) {
    pybind11::class_<Vector<double> > VecClass(obj, "Vector");
    VecClass.def(pybind11::init<int>());
    VecClass.def(pybind11::self += pybind11::self);
    VecClass.def("__getitem__", &Vector<double>::operator[]);
    VecClass.def("__setitem__", [](Vector<double>& v, int i, double x) {v[i] = x;});
    VecClass.def("__str__", &Vector<double>::toString);
    VecClass.def("__repr__", &Vector<double>::toString);
    VecClass.def("print", &Vector<double>::print);
}

顺带一提,C++的模版在这里几乎起不到作用,因为Pybind11必须将模版实例化才能进行绑定,和没有模版几乎没区别。

编译

本人已经放弃CMake了,不能理解CMake的逻辑,还要记很多东西,不如直接Makefile。所以这里写的是直接按命令编译。

含Pybind11的C++代码编译命令是

$ c++ -O3 -Wall -shared -std=c++11 -fPIC $(python3 -m pybind11 --includes) example.cpp -o example$(python3-config --extension-suffix)

其实很简单对吧。标准需要在C++11以上,$(python3 -m pybind11 --includes)是pybind11的头文件include路径。顺带一提,python-dev的include路径是$(python3-config --includes),链接库选项是$(python3-config --ldflags)$(python3-config --extension-suffix)是与python版本和系统相关的后缀名,在我的电脑上以.so结尾。

特别提示,在Darwin(MacOS)上需要再加上-undefined dynamic_lookup。这是一个很神奇的选项,我也不太理解它干什么用,好像是跳过编译阶段的undefined symbols,等到链接时再寻找。

运行

只要这个库在python脚本的同一目录或者PYTHONPATH中,python就可以直接import它。模块名称是PYBIND11_MODULE的第一个参数。

附加环节:使用pyi进行代码提示

使用C++生成的python库运行是没有问题的,但是不会有任何的代码补全和提示。主流IDE都是使用pyi文件来进行代码提示的,github上的项目pybind11-stubgen能够做到生成任意python模块的pyi文件。

在库文件的同一目录下运行pybind11-stubgen,比如对于上面的vector模块,执行

pybind11-stubgen vector --output-dir . --no-setup

它将会生成一个文件夹,里面包含了一个__init__.pyi。这就是我们需要的东西。如果把--no-setup去掉,还会生成一个setup.py,但我不知道这有啥用。

要有条理地使用这个pyi文件,可以将库打包成一个包。也就是新建一个文件夹,把库文件和pyi塞进去,然后再加一个__init__.py使其成为包。注意,__init__.py中需要导入__init__.pyi__all__里包括的变量才能使代码提示正常工作。

(可能也可以使用那个setup.py

踩坑

第一个坑:-undefined dynamic_lookup。上面说过了。

第二个坑:最好使cpp的文件名、PYBIND11_MODULE的模块名、库文件的前缀三者保持一致,cpp的类名/函数名和PYBIND11_MODULE中绑定的python类名/函数名一致。其实不一致也没什么意义,但是不一致的话很容易在导入包的时候发生错误,最后还是不要这么做。

第三个坑:官方提供了操作符重载的简便写法

#include <pybind11/operators.h>

PYBIND11_MODULE(example, m) {
    py::class_<Vector2>(m, "Vector2")
        .def(py::init<float, float>())
        .def(py::self + py::self)
        .def(py::self += py::self)
        .def(py::self *= float())
        .def(float() * py::self)
        .def(py::self * float())
        .def(-py::self)
        .def("__repr__", &Vector2::toString);
}

希望没有其他朋友像我一样漏看了第一行的头文件,导致py::self找不到google了半个小时。

C++ vs NumPy

都写到这里了,我觉得可以解决一个我长时间以来不理解的问题:

Python比C++慢吗?慢多少?使用C++混合编程是否确实能提高速度?

我用上面的Vector类简单进行了一个测试。进行比较的操作符是运算后赋值(+=)。具体来说,是这样的:

from cpp_vec import Vector
import random
import time
import numpy as np

x = Vector(100000)
y = Vector(100000)
# assign each element in x with random double precision number
for i in range(100000):
    x[i] = random.random()
    y[i] = random.random()

# start timing
start = time.time()
for i in range(100):
    x += y
end = time.time()
print("Time elapsed by cpp_vec: %f" % (end - start))

x = np.zeros(100000)
y = np.zeros(100000)
for i in range(100000):
    x[i] = random.random()
    y[i] = random.random()

start = time.time()
for i in range(100):
    x += y
end = time.time()
print("Time elapsed by numpy: %f" % (end - start))

我在C++里开启了OpenMP(上面的代码里面写了),这是公平的,因为NumPy本身也是C实现的,它能够绕过GIL锁来使用多线程。上面的设置导致的结果是

Time elapsed by cpp_vec: 0.013195
Time elapsed by numpy: 0.004841

但如果将数组的大小开到10000000,循环次数取消(保证理论上的计算量一样),得到的结果是

Time elapsed by cpp_vec: 0.011262
Time elapsed by numpy: 0.016986

C++在大规模的代数运算上还是有优势的(何况这只是非常naive的一个实现)。但是Pybind11带来的调用开销可能是比较大的,导致反复调用的开销还是比较大。

当然,这只是个人解释。后面说不定还有什么妙の原因,之后再探索吧。

标签:__,Python,self,py,C++,Vector,Pybind11,def
From: https://www.cnblogs.com/undermyth/p/17650691.html

相关文章

  • Python基础入门学习笔记 064 GUI的终极选择:Tkinter
    >>>importtkinter #Tkinter是python默认的GUI库,导入Tkinter模块>>> 实例1:1importtkinterastk23root=tk.Tk()#创建一个主窗口,用于容纳整个GUI程序4root.title("FishCDemo")#设置主窗口对象的标题栏56#添加一个Label组件,可以显示文本、图标或者图......
  • Python基础入门学习笔记 065 GUI的终极选择:Tkinter2
    实例1:Label组件显示文字与gif图片1#导入tkinter模块的所有内容2fromtkinterimport*34#创建主窗口5root=Tk()6#创建一个文本Label对象,文字为左对齐,离左边边框距离为107textLabel=Label(root,8text="您下载的影片含有未成年人......
  • Python基础入门学习笔记 048 魔法方法:迭代器
    迭代的意思类似于循环,每一次重复的过程被称为一次迭代的过程,而每一次迭代得到的结果会被用来作为下一次迭代的初始值。提供迭代方法的容器称为迭代器(如序列(列表、元组、字符串)、字典等)。对一个容器对象调用iter()就得到它的迭代器,调用next()迭代器就会返回下一个值。入托迭代器没......
  • Python基础入门学习笔记 049 乱入:生成器
    所谓协同程序,就是可以运行的独立函数调用,函数可以暂停或者挂起,并在需要的时候从程序离开的地方继续或者重新开始。生成器可以暂时挂起函数,并保留函数的局部变量等数据,然后在再次调用它的时候,从上次暂停的位置继续执行下去。一个函数中如果有yield语句,则被定义为生成器。实例1:......
  • Python基础入门学习笔记 050 模块:模块就是程序
    什么是模块•容器->数据的封装•函数->语句的封装•类->方法和属性的封装•模块->模块就是程序命名空间爱的宣言:世界上只有一个名字,使我这样牵肠挂肚,像有一根看不见的线,一头牢牢系在我心尖上,一头攥在你手中,这个名字就叫做鱼C工作室计算机一班的小花……导入模块•......
  • C++ LibCurl 库的使用方法
    LibCurl是一个开源的免费的多协议数据传输开源库,该框架具备跨平台性,开源免费,并提供了包括HTTP、FTP、SMTP、POP3等协议的功能,使用libcurl可以方便地进行网络数据传输操作,如发送HTTP请求、下载文件、发送电子邮件等。它被广泛应用于各种网络应用开发中,特别是涉及到数据传输的场景。......
  • Python学习 -- 类的继承
    类继承是面向对象编程中的重要概念,它允许我们创建一个新的类,通过继承已有的类的属性和方法,从而实现代码的重用和扩展。Python作为一门面向对象的编程语言,提供了强大而灵活的类继承机制。本篇博客将深入探讨Python中类继承的概念,通过详细的代码实例演示其用法。基本的类继承首先,让我......
  • Python基础入门学习笔记 039 类和对象:拾遗
    组合(将需要的类一起进行实例化并放入新的类中)实例:1classTurtle:2def__init__(self,x):3self.num=x45classFish:6def__init__(self,x):7self.num=x89classPool:10def__init__(self,x,y):11self.tu......
  • Python-OpenCV双目测距代码实现以及参数解读
    一、双目相机拍照后使用Matlab进行双目标定必看:USB双目相机的具体标定过程:https://blog.csdn.net/qq_40700822/article/details/124251201?spm=1001.2014.3001.5501主要参考:https://blog.csdn.net/dulingwen/article/details/98071584感谢大佬的分享!!!(*≧ω≦)!!二、标定后生成......
  • Python基础入门学习笔记 040 类和对象:一些相关的BIF
    一些相关的BIFissubclass(class, classinfo) 如果第一个参数(class)是第二个参数(classinfo)的一个子类,则返回True,否则返回False1>>>classA:2pass34>>>classB(A):5pass67>>>issubclass(B,A)8True9>>>issubclass(B,B)#......