本文最后更新于:2024年12月3日 下午
C++与Python合作的方法
C/C++
是老牌的编程语言,可能是许多人入门计算机学的第一门语言,生活中的各种软件、应用、系统的底层都离不开它,其运行效率也比许多语言更快,但是C++
是一个静态类型的语言,简单来说就是其灵活性不够。
Python是目前十分流行的高级编程语言,可能很多人大学里也学习过Python,随着大数据、人工智能、深度学习的火热,更灵活的具有动态类型的Python语言被更多人青睐。Python强调代码的可读性和简洁的语法,相比于C语言或Java,它让开发者能够用更少的代码表达想法,但是通常来说其效率要比其它语言慢很多。
为了兼顾效率和灵活性,许多系统都使用CPP+Python合作的方式构建,本文对官方CPython提供的C/C++扩展方法进行简单的介绍。
使用C++调用Python
Hello World
官方文档:https://docs.python.org/zh-cn/3/extending/extending.html
首先介绍一下环境,笔者在自己的Windows笔记本下跑的demo,用的Python3.11,编译器是MinGW,IDE是CLion,构建工具用CMake。
第一个目标是用C++调用Python输出Hello World,首先写main.cpp
:
1 2 3 4 5 6 7 8 9
| #include <Python.h>
int main() { Py_Initialize(); PyRun_SimpleString("print('Hello, World!')"); Py_Finalize(); return 0; }
|
Python.h
包含了一些标准头文件: <stdio.h>
,<string.h>
,<errno.h>
和 <stdlib.h>
,在包含任何标准头文件之前,必须先包含 Python.h
。
这个代码没什么好说的,就是一个最简单的模板,也不涉及py文件的导入,而是直接用文本来写python代码。
为了编译这个文件,我们要找到Python的路径,比如我的路径就在:
C:\Users\<用户名>\AppData\Local\Programs\Python\Python311
在这个路径下的include
中有Python.h
,在libs
下有python311.dll
链接库。
根据这些信息,写下CMakeLists.txt
:
1 2 3 4 5 6 7 8 9 10 11
| cmake_minimum_required(VERSION 3.19) project(pythoncpp)
set(CMAKE_CXX_STANDARD 11)
include_directories(./) include_directories("C:/Users/<用户名>/AppData/Local/Programs/Python/Python311/include") link_directories("C:/Users/<用户名>/AppData/Local/Programs/Python/Python311/libs") link_libraries("python311")
add_executable(main main.cpp)
|
然后就可以用IDE跑起来了!顺利的话,IDE会输出:
调用Python文件中的函数
接下来在py/t1.py
中写一个简单的函数:
1 2 3
| def print_hello_world(): print('hello world') return "Done"
|
那么调用它的cpp代码就会复杂一些:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #include <iostream> #include <Python.h>
int main() { Py_Initialize(); if (!Py_IsInitialized()) {return 1;} PyRun_SimpleString("import sys"); PyRun_SimpleString("sys.path.append('../py')"); PyObject* module = PyImport_ImportModule("t1"); if (module == nullptr) { std::cout << "Module not found: t1" << std::endl; return 1; } PyObject* func = PyObject_GetAttrString(module, "print_hello_world"); if (!func || !PyCallable_Check(func)) { std::cout << "Function not found: print_hello_world" << std::endl; return 1; } PyObject* result = PyObject_CallObject(func, nullptr); std::string result_str = PyUnicode_AsUTF8(result); std::cout << result_str << std::endl; Py_Finalize(); return 0; }
|
import sys
和sys.path.append('../py')
保证了python能够在对应路径下找到你写的python文件,后面则是利用API来import t1这个module,然后调用module中print_hello_world这个方法,并获取返回值输出。
可以看到,由于Python万物皆对象,模块、方法、变量都是PyObject
的类型,PyObject_GetAttrString
就像.
操作符,PyObject_CallObject
就像__call__()
,而Python中的变量要变成C++的数据结构,要用专门的转换函数。
动态传参
假如调用的函数有参数:
1 2
| def add(a, b): return a + b
|
那么就需要打包参数再调用(下面代码不全,前面获取模块和结束部分都省略了):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| PyObject* args = PyTuple_New(2); PyTuple_SetItem(args, 0, Py_BuildValue("i", 123)); PyTuple_SetItem(args, 1, Py_BuildValue("i", 666)); PyObject* result = PyObject_CallObject(func, args); int result_num; PyArg_Parse(result, "i", &result_num); std::cout << result_num << std::endl;
PyObject* args2 = PyTuple_New(2); PyTuple_SetItem(args2, 0, Py_BuildValue("s", "123")); PyTuple_SetItem(args2, 1, Py_BuildValue("s", "666")); PyObject* result2 = PyObject_CallObject(func, args2); std::string result_str = PyUnicode_AsUTF8(result2); std::cout << result_str << std::endl;
|
由于python的add可以加数字,也可以加字符串, 所以只需要给它传不同的参数。
调用类中的方法
难度升级,我们整一个类,要调用print_index方法:
1 2 3 4 5
| class TestClass: def __init__(self, index): self.index = index def print_index(self): print(self.index)
|
然而实际上对应的cpp代码也不难:
1 2 3 4 5 6
| PyObject* cls = PyObject_GetAttrString(module, "TestClass"); PyObject* args = PyTuple_New(1); PyTuple_SetItem(args, 0, Py_BuildValue("i", 123)); PyObject* instance = PyEval_CallObject(cls, args); PyObject* method = PyObject_GetAttrString(instance, "print_index"); PyObject_CallObject(method, nullptr);
|