1 背景
python被称为胶水语言,其优势是能够粘结各种不同的语言。同时,python有着更大的“亲民性”,很容易进行开发。但是,python最大的问题就是计算速度不够。通常可以用CUDA或者C++对一个python程序进行加速,加速策略如下:
大规模算术运算、矩阵运算等过程用底层语言这里使用C++编写,python只负责传参和处理结果数据;
十分常用的函数,我们可以用C++写成“算子”,然后python调用算子即可,如边缘检测的Sobel算子;
最终实现方式:
C++代码编写完成后,编译为.so文件
python调用.so文件中的函数等,实现编程开发效率的提升(python可以更高效的完成接口或者数据处理的代码开发)和运行速度的提升(C++运行算子速度快)
2 一个最简单的例子和原理说明与注意事项
2.0 最简单的例子
原始CPP代码hello_world.cpp
#include <iostream>#include <string>using namespace std;int test(int n){ cout<<"The C++ input is "<<n<<"\n"; cout<<"The C++ output is "<<n+100<<"\n"; return n+100;}
C++的函数需要用extern描述
才能被Python调用。将代码修改为如下形式的CPP文件,如下:
#include <iostream>#include <string>using namespace std;extern "C"{ int test(int n){ cout<<"The C++ input is "<<n<<"\n"; cout<<"The C++ output is "<<n+100<<"\n"; return n+100; }}
在Linux环境下编译:
g++ -o hello_world.so -shared -fPIC hello_world.cpp
备注
编译cpp生成动态库。编译方法可以是直接上g++,也可以用CMake。-o 指定生成的文件名-shared 指定微共享库-fPIC 表明使用地址无关代码根据上面的编译命令,就可以在同目录下得到名为hello_world.so
的文件了,这就是可以被python直接调用的。再来看看python调用的代码C++_call.py
:
原理:
通过ctypes
的对.so
的文件进行实例化,使得这个方法可以在python引用,类比为一个函数同时对这个实例化的方法进行,输入输出的类型定义,去用ctypes
中的类型去规定这个函数的输入输出接口 import ctypesll = ctypes.cdll.LoadLibrarylib = ll('./hello_world.so')lib.test.argtypes = [ctypes.c_int] # 对C++库在python中的实例进行进一步的输入参数类型的定义lib.test.restype = ctypes.c_int # 对C++库在python中的实例进行进一步的输出类型的定义cpp_out = lib.test(100)print(cpp_out)
这样就成功实现在无传参情况下python调用C++的函数。输出如下
2.1 原理说明
为什么要用extern "C"
使用C++来编译so库,需要注意一点,C++中的函数需要extern "C"
来转成C语法编译,因为C++函数是可以重载的,使用g++编译函数会附上额外信息而不是函数名本身,比方void print(int a)
;使用g++编译会生成print_int
之类的,这样cdll.LoadLibrary
的时候会找不到。所以,我们需要让编译器使用C方法编译,这样才能达到目的。这也是值得我们请注意的一点。 另外根据如上原理其实extern "C"
只需要加在需要被python调用的C++函数外面即可。其他不被调用的可以不用加。
ctypes
的原理 调用Python的自有模块ctypes
,其中cdll = <ctypes.LibraryLoader object>
是一个库加载器对象,调用cdll.LoadLibrary
便可调用C++的so库。
2.2 注意事项
如果python在调用C函数内部出现了问题,系统不会提示具体出现什么问题,只会提示"segmentation fault"。所以最好是先用C语言调用该动态库验证没有问题了再提供给python调用。
python传参给C函数时,可能会因为python传入实参与C函数形参类型不一致会出现问题( 一般int, string不会有问题,float要注意2.0中的例子里面int其实可以不用转换,但是为了举例和说明我们设置了一下)。这时需要在python调用时传入的实参做一个类型转换(见第三部分)
3 简单的python调用C++实例
3.1 无输入输出参数情况
C++代码hello_world.cpp
#include <iostream>using namespace std;extern "C"{ int main(){ cout<<"hello world\n"; }}
python调用的代码:
import ctypesll = ctypes.cdll.LoadLibrarylib = ll('./hello_world.so')lib.main()
这样就成功实现在无传参情况下python调用C++的函数。输出如下
3.2 有输入参数的情况
C++函数如果带输入参数
#include <iostream>#include <string>using namespace std;extern "C"{ void test(int n){ cout<<"The input is "<<n<<"\n"; }}
编译完成后,python调用代码
import ctypesll = ctypes.cdll.LoadLibrarylib = ll('./hello_world.so')lib.test(100)
3.3 有输出参数的情况
C++函数如果带输入参数
#include <iostream>#include <string>using namespace std;extern "C"{ int test(int n){ cout<<"The C++ input is "<<n<<"\n"; cout<<"The C++ output is "<<n+100<<"\n"; return n+100; }}
编译完成后,python调用代码
import ctypesll = ctypes.cdll.LoadLibrarylib = ll('./hello_world.so')cpp_out = lib.test(100)print(cpp_out)
3.4 多输入参数的情况
C++代码
#include <iostream>#include <string>using namespace std;extern "C"{ void test(int n1, int n2, int n3){ cout<<"The C++ input 1 is "<<n1<<"\n"; cout<<"The C++ input 2 is "<<n2<<"\n"; cout<<"The C++ input 3 is "<<n3<<"\n"; }}
python调用代码
import ctypesll = ctypes.cdll.LoadLibrarylib = ll('./hello_world.so')lib.test(100,200,300)
4 python与C++调用的类型转换
4.0 类型转换表
ctypes — A foreign function library for Python — Python 3.11.1 documentation
CType(Cpp经过extern转换成C后的类型) | Python Type(python中的类型) | ctypes Type(python中调用ctypes后实例化的类型) |
---|---|---|
_Bool | bool (1) | c_bool |
char | 1-character string | c_char |
wchar_t | 1-character Unicode string | c_wchar |
char | int/long | c_byte |
unsigned char | int/long | c_ubyte |
short | int/long | c_short |
unsigned short | int/long | c_ushort |
int | int/long | c_int |
unsigned int | int/long | c_uint |
long | int/long | c_long |
unsigned long | int/long | c_ulong |
__int64long long | int/long | c_longlong |
unsigned __int64 or unsigned long long | int/long | c_ulonglong |
float | float | c_float |
double | float | c_double |
long double | float | c_longdouble |
char * (NULL terminated) | string or None | c_char_p |
wchar_t * (NULL terminated) | unicode or None | c_wchar_p |
void * | int/long or None | c_void_p |
4.1 float的传入传出
如果还是使用之前的代码进行类似的直接传递就会出现如下问题
C++代码如下
#include <iostream>#include <string>using namespace std;extern "C"{ float test(float n){ cout<<"The C++ float input is "<<n<<"\n"; cout<<"The C++ float output is "<<n+100<<"\n"; return n+100; }}
python调用代码如下
import ctypesll = ctypes.cdll.LoadLibrarylib = ll('./hello_world.so')cpp_out = lib.test(100)print(cpp_out)
即使Python转换成python中的float也无济于事
所以就需要在代码中进行类型转换
import ctypesll = ctypes.cdll.LoadLibrarylib = ll('./hello_world.so')lib.test.argtypes = [ctypes.c_float] # 对C++库在python中的实例进行进一步的输入参数类型的定义lib.test.restype = ctypes.c_float # 对C++库在python中的实例进行进一步的输出类型的定义cpp_out = lib.test(100)print(cpp_out)
同时还有一种方法:原理是使用ctype对python中的变量进行转换成一个新的变量,然后直接送到接口里面
import ctypesll = ctypes.cdll.LoadLibrarylib = ll('./hello_world.so')input_num = ctypes.c_float(100)lib.test.restype = ctypes.c_float # 对C++库在python中的实例进行进一步的输出类型的定义cpp_out = lib.test(input_num)print(cpp_out)
输出结果同上
4.2 数据数组的传入传出
对于返回值为数组的情况,可以直接使用索引去访问,但是下标操作[]不是从迭代器中取对象,而是地址偏移
4.2.1 第一种方法(指定参数类型完成转换)
C++代码
#include <iostream>#include <string>using namespace std;extern "C"{ float* test(float* n){ cout<<"The C++ input list is [0]:"<<n[0]<< "[1]:" << n[1] << "[2]:" << n[2] <<"\n"; for(int i=0;i<3;i++){ n[i] = n[i]+1; } return n; }}
python调用代码1(介绍一下两个carry_in的方式)
import ctypesimport numpy as npll = ctypes.cdll.LoadLibrarylib = ll('./hello_world.so')pyarray_in = [0.5,1.5,2.5]carrary_in = (ctypes.c_float * len(pyarray_in))(*pyarray_in)lib.test.argtypes = [ctypes.POINTER(ctypes.c_float*3)]lib.test.restype = ctypes.POINTER(ctypes.c_float*3)carray_out = lib.test(carrary_in) # 对指针类型数据的获取方法1print(carray_out.contents[0])print(carray_out.contents[1])print(carray_out.contents[2])# 对指针类型数据的获取方法2for i in range(len(carrary_in)): print(carrary_in[i])
python调用代码2(第二种数组的实例化转换方式)
import ctypesimport numpy as npll = ctypes.cdll.LoadLibrarylib = ll('./hello_world.so')#设置数组类型,注意下面的3好像无法换成变量表示,但是给数组赋值时可以用变量赋值Arry_Type = ctypes.c_float * 3carrary_in = Arry_Type(0.5,1.5,2.5)lib.test.argtypes = [ctypes.POINTER(ctypes.c_float*3)]lib.test.restype = ctypes.POINTER(ctypes.c_float*3)carray_out = lib.test(carrary_in) # 对指针类型数据的获取方法1print(carray_out.contents[0])print(carray_out.contents[1])print(carray_out.contents[2])# 对指针类型数据的获取方法2for i in range(len(carrary_in)): print(carrary_in[i])
4.2.2 第二种方法(使用numpy的封装进行类型转换)
C++代码不变
python调用代码,有如下特点
可以使用内嵌的ndpointer类进行接口的定义可以对输出进行直接的转换,不需要根据索引去查找内存但是在定义接口的时候尤其是输出,是一定需要定义维度的,否则就会报错import ctypesimport numpy as npfrom numpy.ctypeslib import ndpointer,as_arrayll = ctypes.cdll.LoadLibrarylib = ll('./hello_world.so')pyarray = np.array([1,2,3], dtype="float32")lib.test.argtypes = [ndpointer(ctypes.c_float)]lib.test.restype = ndpointer(ctypes.c_float, shape=(3,))array_out = lib.test(pyarray)print(as_array(array_out))
4.3 字符型变量的输入输出
C++代码
#include <iostream>#include <string>using namespace std;extern "C"{ char test(char n){ cout<<"The C++ input char is "<< n <<"\n"; cout<<"The C++ output char is "<< char(n+1) <<"\n"; return n+1; }}
python调用代码
注意事项需要在类型转换的过程中加入对应的编码等信息
import ctypesimport numpy as npll = ctypes.cdll.LoadLibrarylib = ll('./hello_world.so')lib.test.argtypes = [ctypes.c_char]lib.test.restype = ctypes.c_charcpp_out = lib.test('a'.encode('utf-8')) print(cpp_out)print(cpp_out.decode())
4.4 字符串变量的输入输出
C++代码
#include <iostream>#include <string>using namespace std;extern "C"{ char* test(char* n){ cout<<"The C++ input char is "<< n ; char * n_out; for (int i=0; i<4; i++) { n_out[i] = char(n[i]-32); } return n_out; }}
python调用代码(使用字符串形式)
import ctypesll = ctypes.cdll.LoadLibrarylib = ll('./hello_world.so')lib.test.argtypes = [ctypes.c_char_p]lib.test.restype = ctypes.c_char_pcpp_out = lib.test('abcd'.encode('utf-8')) print(cpp_out.decode())
python调用代码(使用字符型指针类型)
import ctypesll = ctypes.cdll.LoadLibrarylib = ll('./hello_world.so')lib.test.argtypes = [ctypes.c_char_p]lib.test.restype = ctypes.c_char_pcpp_in=(ctypes.c_char * 4)(*bytes("abcd",'utf-8'))cpp_out = lib.test(cpp_in) print(cpp_out.decode())
python调用代码(使用字符型指针类型作为输入输出的转换,同时实例变量方式)
import ctypesll = ctypes.cdll.LoadLibrarylib = ll('./hello_world.so')lib.test.argtypes = [ctypes.c_char_p]lib.test.restype = ctypes.c_char_pcpp_out = lib.test(bytes("abcd",'utf-8')) print(cpp_out.decode())
4.5 图片的传入传出
其实原理和4.2.2一样 使用numpy的方式不一样
可以概括为如下伪代码
lib.C++函数名称.argtypes = [ndpointer(dtype=ctypes.c_int)]lib.C++函数名称.restype = ctypes.POINTER(ctypes.c_int)如果输出是要使用ndpointer的话需要定义维度lib.C++函数名称.restype = ndpointer(ctypes.c_int, shape=(3,3))
python调用.so - 走看看
【python的numpy数组(c++接口PyArrayObject*) 和c++的Mat的相互转换】_无情的AI鸽子的博客-CSDN博客_python 调用c++ mat转换
Python中Numpy数组转换成C++中OpenCV的Mat类型(超少代码,超快速度)_Colin_Jing的博客-CSDN博客_numpy转mat
python 调用c++处理数组和图片_koibiki的博客-CSDN博客
4.6 二维数组
C++代码
#include <iostream>#include <string>using namespace std;extern "C"{ void show_matrix(int *matrix, int rows, int columns) { int i, j; for (i=0; i<rows; i++) { for (j=0; j<columns; j++) { printf("matrix[%d][%d] = %d\n", i, j, matrix[i*rows + j]); } } }}
python调用代码
import ctypesimport numpy as np lib = ctypes.cdll.LoadLibrary("./hello_world.so")arr = np.random.randint(0, 10, size = (3,5))print(arr)#arr = np.array([[1,2],[3,4]])tmp = np.asarray(arr)rows, cols = tmp.shapedataptr = tmp.ctypes.data_as(ctypes.c_char_p)lib.show_matrix(dataptr, rows, cols)
ctypes的运用(把一个numpy数组传入c中)_科研路上的小C的博客-CSDN博客_ctypes numpy
4.7 批量自适应python类型装换代码
写了一个总的接口,之后可以针对不同类型进行不不同的转换,然后直接送进去就好了
#将python类型转换成c类型,支持int, float,string的变量和数组的转换def convert_type(input): ctypes_map = {int:ctypes.c_int, float:ctypes.c_double, str:ctypes.c_char_p } input_type = type(input) if input_type is list: length = len(input) if length==0: print("convert type failed...input is "+input) return null else: arr = (ctypes_map[type(input[0])] * length)() for i in range(length): arr[i] = bytes(input[i],encoding="utf-8") if (type(input[0]) is str) else input[i] return arr else: if input_type in ctypes_map: return ctypes_map[input_type](bytes(input,encoding="utf-8") if type(input) is str else input) else: print("convert type failed...input is "+input) return null# 使用直接将convert_type(python变量)送到so的库里面的函数中即可eglib.test(convert_type(python变量))
python3调用cpp的方法——python调用so_springlustre的博客-CSDN博客_python cpp
4.8 结构体的传入传出
python调用c++ ctype list传数组或者返回数组的方法 - 经验笔记
python 调用c++处理数组和图片_koibiki的博客-CSDN博客
python调用C++问题解决1(PEP 3118 buffer format string)
ValueError: ‘<P’ is not a valid PEP 3118 buffer format string
可能原因1:一般是因为python和numpy的版本不对,更新一下即可可能原因2:输出的接口处没有定义好维度信息(详见4.2.2)如何在python3 ctype中返回ndarray? - 问答 - 腾讯云开发者社区-腾讯云python调用C++问题解决2(cannot dynamically load executable)
可能原因1:动态库编译方式有问题,加入-shared
可能原因2:也有可能是有可执行文件,可能需要把之前的可执行文件删掉了之后进行重新生成
cannot dynamically load executable的尴尬经历_Aero Auto的博客-CSDN博客_cannot dynamically load
python调用C++问题解决3(windows生成DLL后的使用)
python调用C++ DLL 传参技巧_冰雪满天的博客-CSDN博客
python 调用C++ DLL,传递int,char,char*,数组和多维数组 - quanzhan - 博客园
5 Python调用C++的另一种方式pybind11
pybind11 直接支持: 在python端传入list或numpy数据,c++中计算得到的vector或string结果也可以便捷传出
python调用c++模块.so库, 互相回传数据(ctypes、pybind11) - 知乎
6 使用cpython 调用 C/C++ 函数或者类
原理其实和Ctypes基本一致也是需要重定义接口的类型转换,但是这里可能定义文件都是在另一个文件中了,好像更加自由便捷一些,编译方式和之后的调用方式也有一些区别
Python 调用 C++ | iqhy’s Blog
速度测试
Cpp代码
#include <iostream>#include <string>using namespace std;extern "C"{ int test(int n){ cout<<"The C++ input is "<<n<<"\n"; int result = 0; for (int i=0;i<n;i++){ result = result + 0; } cout<<"The C++ output is "<<result<<"\n"; return result; }}
python调用以及时间测试
from time import timen = 1000000result = 0s1 = time()for i in range(n): result = result + 0print(result)s2 = time()print(s2-s1)import ctypesll = ctypes.cdll.LoadLibrarylib = ll('./hello_world.so')input_num = ctypes.c_int(1000000)lib.test.restype = ctypes.c_int # 对C++库在python中的实例进行进一步的输出类型的定义s1 = time()cpp_out = lib.test(input_num)print(cpp_out)s2 = time()print(s2-s1)
其实可以发现速度差距还是很大的,这只是简单循环所带来的差距
结论:
Python为C/C++语言提供了良好的扩展机制,这对于Python本身来说是至关重要的,因为这对于使用Python语言开发的项目来说,在特定条件下嵌入C/C++代码很有帮助,对于整个项目的开发和运用都是大有裨益的。
参考文献
python调用C++中的函数【最简明教程】_木盏的博客-CSDN博客_python调用c++函数
Python调用C/C++的两种方法 - 知乎
Python调用C++动态链接库返回数组 - 空‘ - 博客园
C++ 从函数返回数组 | 菜鸟教程
How do I convert a Python list into a C array by using ctypes? - Stack Overflow
如何在python3 ctype中返回ndarray? - 问答 - 腾讯云开发者社区-腾讯云
Value Error: ‘<P’ is not a valid PEP 3118 buffer format string · Issue #2 · rpoleski/MulensModel
python - PEP 3118 warning when using ctypes array as numpy array - Stack Overflow
Python中Numpy数组转换成C++中OpenCV的Mat类型(超少代码,超快速度)_Colin_Jing的博客-CSDN博客_numpy转mat
ctypes的运用(把一个numpy数组传入c中)_科研路上的小C的博客-CSDN博客_ctypes numpy
cannot dynamically load executable的尴尬经历_Aero Auto的博客-CSDN博客_cannot dynamically load
python调用c++模块.so库, 互相回传数据(ctypes、pybind11) - 知乎
python3调用cpp的方法——python调用so_springlustre的博客-CSDN博客_python cpp
Python 调用 C++ | iqhy’s Blog
python 调用c++处理数组和图片_koibiki的博客-CSDN博客
How do I convert a Python list into a C array by using ctypes? - Stack Overflow
Python如何调用DLL函数:C数组与numpy数组传递 - 腾讯云开发者社区-腾讯云
python调用C函数时的数组传递_左左张的博客-CSDN博客_python 传递数组给c
python调用C++ DLL 传参技巧_冰雪满天的博客-CSDN博客
python 调用C++ DLL,传递int,char,char*,数组和多维数组 - quanzhan - 博客园
python调用c++传递数组-云社区-华为云
python 调用C++ DLL,传递int,char,char*,数组和多维数组 - quanzhan - 博客园
怎么在python中通过调用c++传递数组 - 开发技术 - 亿速云
Python调用C++ 传数组指针参数 - 知乎
pool python 传参数_Python调用C++ 传数组指针参数_weixin_39942492的博客-CSDN博客
之后可以参考的官方文档
C型外部功能接口 (numpy.ctypeslib ) — NumPy v1.21.dev0 Manual
NumPy API(二十八)——C-Types 外部功能接口 - 简书
C-Types外部函数接口(numpy.ctypeslib
) - NumPy 中文文档
1. 使用 C 或 C++ 扩展 Python — Python 3.7.13 文档
使用Python作为粘合剂 - NumPy 中文文档
ctypes — Python 的外部函数库 — Python 3.11.1 文档