C++与Python合作的方法

本文最后更新于: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(); // 初始化 Python 解释器
PyRun_SimpleString("print('Hello, World!')"); // 调用Python输出Hello World
Py_Finalize(); // Py_Initialize() 的逆向操作,结束
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;}
// 添加py文件搜索路径
// 由于生成的exe是在cmake-build-debug文件夹下,所以是../py
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 syssys.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)); // 设置参数的值,i表示int
PyTuple_SetItem(args, 1, Py_BuildValue("i", 666)); // 设置参数的值,i表示int
PyObject* result = PyObject_CallObject(func, args); // 的第二个参数就是元组
int result_num;
PyArg_Parse(result, "i", &result_num); // 把结果放到int变量中
std::cout << result_num << std::endl;

// 调用函数
PyObject* args2 = PyTuple_New(2); // 参数用元组打包
PyTuple_SetItem(args2, 0, Py_BuildValue("s", "123")); // 设置参数的值,s表示string
PyTuple_SetItem(args2, 1, Py_BuildValue("s", "666")); // 设置参数的值,s表示string
PyObject* result2 = PyObject_CallObject(func, args2); // PyObject_CallObject的第二个参数就是元组
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); // __init__初始化
PyObject* method = PyObject_GetAttrString(instance, "print_index"); // 获取方法
PyObject_CallObject(method, nullptr); // 调用方法