C++调用Python方法详解(同样适用导入第三方包的python脚本)

前言

近期的项目有从C++调用python脚本的需求,且Python脚本为引用了很多第三方包的复杂脚本,在网上查了很多资料都不全,很少涉及复杂脚本的引用(很多都是简单无第三方包的脚本),经过努力现已解决,故将解决思路写于此处

环境准备

  1. Microsoft Visual Studio (笔者使用版本为2022 社区版)
  2. 确保机器安装了python解释器(笔者使用的为conda中的python3.10)
  3. 确保要调用的python脚本的依赖包都已导入(即都pip成功了)
  4. 运行时记得选择release模式,而不是debug!!

在VS中配置依赖库

创建新项目        

        在visual studio中创建一个新项目,如控制台应用程序(不建议选择空项目,可能会在配置依赖库时缺少选项)

3fee6c69066e41ee94eb352661dd9cf1.png

配置链接目录

        右键点击项目,选择属性,开始配置链接目录

d9738c0e457146f2bc21c54967f75ebf.png

设置附加包含目录

        在“C/C++”选项卡下,找到“常规”部分,设置“附加包含目录”,将python解释器的包含目录(也就是python安装路径下的include文件夹)填入

注:笔者的python.exe路径为C:\Users\13680\.conda\envs\testForCpp2Py\python.exe,可作为相对路径的参考

410929d8e8014908b0ec7eb90b899a2f.png

找到待调用python脚本的环境下的python解释器的路径(一定要是相同python解释器版本),python.exe同级目录下的include文件夹就是我们的目标路径,将该路径复制后添加到“附加包含目录”这里(笔者此处路径为“C:\Users\13680\.conda\envs\testForCpp2Py\include”)

e289fea8f47043f197090dadde03faa0.png

f0381cd1d27641da873c2aac69c73a25.png

设置附加库目录

在“链接器”选项卡下,找到“常规”部分,设置“附加库目录”,将python解释器的库目录(也就是python安装路径下的libs文件夹)填入

e819e7f858df45c19cb60bccacd38e3d.png

将该libs文件夹的路径填入(笔者此处路径为“C:\Users\13680\.conda\envs\testForCpp2Py\libs”)

44edd1067f9140e8b9dfb2b331deea69.png

设置附加依赖项

在“链接器”选项卡下,找到“输入”部分,设置“附加依赖项”,将python解释器的对应的lib文件(也就是python安装路径下的libs文件夹中的python310.lib文件【根据python版本不同名称也不同,大致格式为pythonXXX.lib】)填入

ff4f2c08ff7d4f6db0cff62345a60092.png

fe347c14f0b940d0a4039784b4aa729b.png

python脚本准备

编写python脚本

这里笔者简单编写一个python脚本,其中导入了第三方包numpy

import numpy as np

print('enter python success')  # 成功找到该脚本


def test_cpp2py(data):
    """
    一个含参函数,用于测试cpp文件调用含第三方包的python脚本
    :param data: 一个测试列表
    :return:
    """
    data_after_np = np.array(data)
    print('successful, and data is: ', data_after_np)  # 输出
    return [1, 2, 3, 4]  # 这里笔者偷懒就直接传一个固定列表返回给调用方了

将python脚本打包为.pyd文件

先创建一个setup.py文件,里面的内容为

from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules=cythonize("testForCpp2Py.py")  # 这里改为你要打包成.pyd文件的py文件路径
)

然后在终端(控制台)执行以下命令:

python setup.py build_ext --inplace

执行后会在当前目录下生成几个文件,其中testForCpp2Py.cp310-win_amd64.pyd文件是我们的目标文件

63930c25ba8343e687856d23cf225a73.png

至此我们完成了python端的准备,接下来就是编写Cpp接口文件了

编写C++接口

python解释器初始化

首先我们要在C++端初始化一个python解释器

Py_Initialize(); // 初始化 Python 解释器

导入.pyd文件和第三方包目录

然后,在这个步骤中最关键的就是将.pyd文件和第三方包导入,实现原理类似于设置环境变量(不懂也没关系,直接改好路径抄就行)

 

 

// 添加 .pyd 文件所在目录到 Python 路径
PyObject* sysPath = PySys_GetObject("path");
// 下面是为导入.pyd文件做准备
PyObject* pPath = PyUnicode_FromString("C:/Users/13680/PycharmProjects/srpTest"); // 这里改成.pyd文件所在目录的路径,原理类似于环境变量
// 下面是导入python脚本引入的第三方包
PyObject* pPath_packages = PyUnicode_FromString("C:/Users/13680/.conda/envs/srpTestEnd/Lib/site-packages"); // 将路径改为你调用的python脚本的解释器的 Lib/site-packages 目录

// 将上述路径加入python系统路径
PyList_Append(sysPath, pPath);
Py_DECREF(pPath);
PyList_Append(sysPath, pPath_packages);
Py_DECREF(pPath_packages);

注:这里的第三方包路径为python解释器的Lib/site-packages文件夹

39dadf39c2cf4d22b46285dfaab4a491.png

调用.pyd文件

接下来就是正式导入.pyd文件并调用 

注:Py_DECREF()函数用于指针释放,不影响业务

    // 在这里导入你的 .pyd 文件,注意一定不要加后缀,比如testForCpp2Py.cp310-win_amd64.pyd就写testForCpp2Py就行
    PyObject* pModule = PyImport_ImportModule("testForCpp2Py"); 
    // 下面实现对.pyd文件中的函数的操作(前提是前面的操作没问题,pModule不会是空指针
    if (pModule != nullptr) {
        PyObject* pFunc = PyObject_GetAttrString(pModule, "test_cpp2py"); // 获取函数,这里填写要调用的函数名,笔者这里是test_cpp2py
        if (pFunc && PyCallable_Check(pFunc)) { // 判断该函数是否存在且可调用

            // 创建参数,由于c++和python的类型不对应,所以要转化
            // 这里根据传入的vector类型的data来创建一个对应列表list
            PyObject* pData = PyList_New(data.size());
            for (int i = 0; i < data.size(); ++i) {
                PyList_SetItem(pData, i, PyLong_FromLong(data[i])); // 这里要对data[i]转化为python类型
            }

            // 构建一个参数列表,第一个参数为参数列表的参数数量,这里为1
            PyObject* pArgs = PyTuple_Pack(1, pData);
            // 调用该函数
            PyObject* pValue = PyObject_CallObject(pFunc, pArgs); // 调用函数
            Py_DECREF(pArgs);

            // 处理返回值
            if (pValue != nullptr) {
                // 声明一个结果的数组
                vector<long> result{}; 
                // 从pValue中取值
                for (Py_ssize_t i = 0; i < PyList_Size(pValue); ++i) {
                    result.push_back(PyLong_AsLong(PyList_GetItem(pValue, i)));
                }
                Py_DECREF(pValue);
            }
            Py_DECREF(pFunc);
        }
        Py_DECREF(pModule);
    }
    else {
        // 证明有问题,pModule为空指针了
        PyErr_Print(); // 打印错误信息
    }

关闭python解释器

用完后关闭python解释器即可

Py_Finalize(); // 关闭 Python 解释器

完整代码(C++)

#include <iostream>
#include <Python.h>
#include <vector>
using namespace std;


vector<int> test_cpp2py(vector<int> data) {
    Py_Initialize(); // 初始化 Python 解释器

    // 添加 .pyd 文件所在目录到 Python 路径
    PyObject* sysPath = PySys_GetObject("path");
    // 下面是为导入.pyd文件做准备
    PyObject* pPath = PyUnicode_FromString("C:/Users/13680/PycharmProjects/testForCpp2Py"); // 这里改成.pyd文件所在目录的路径,原理类似于环境变量
    // 下面是导入python脚本引入的第三方包
    PyObject* pPath_packages = PyUnicode_FromString("C:/Users/13680/.conda/envs/testForCpp2Py/Lib/site-packages"); // 将路径改为你调用的python脚本的解释器的 Lib/site-packages 目录

    // 将上述路径加入python系统路径
    PyList_Append(sysPath, pPath);
    Py_DECREF(pPath);
    PyList_Append(sysPath, pPath_packages);
    Py_DECREF(pPath_packages);

    // 在这里导入你的 .pyd 文件,注意一定不要加后缀,比如testForCpp2Py.cp310-win_amd64.pyd就写testForCpp2Py就行
    PyObject* pModule = PyImport_ImportModule("testForCpp2Py"); 

    // 声明一个用于接收结果的数组
    vector<int> result{};

    // 下面实现对.pyd文件中的函数的操作(前提是前面的操作没问题,pModule不会是空指针
    if (pModule != nullptr) {
        PyObject* pFunc = PyObject_GetAttrString(pModule, "test_cpp2py"); // 获取函数,这里填写要调用的函数名,笔者这里是test_cpp2py
        if (pFunc && PyCallable_Check(pFunc)) { // 判断该函数是否存在且可调用

            // 创建参数,由于c++和python的类型不对应,所以要转化
            // 这里根据传入的vector类型的data来创建一个对应列表list
            PyObject* pData = PyList_New(data.size());
            for (int i = 0; i < data.size(); ++i) {
                PyList_SetItem(pData, i, PyLong_FromLong(data[i])); // 这里要对data[i]转化为python类型
            }

            // 构建一个参数列表,第一个参数为参数列表的参数数量,这里为1
            PyObject* pArgs = PyTuple_Pack(1, pData);
            // 调用该函数
            PyObject* pValue = PyObject_CallObject(pFunc, pArgs); // 调用函数
            Py_DECREF(pArgs);

            // 处理返回值
            if (pValue != nullptr) {
; 
                // 从pValue中取值
                for (Py_ssize_t i = 0; i < PyList_Size(pValue); ++i) {
                    result.push_back(PyLong_AsLong(PyList_GetItem(pValue, i)));
                }
                Py_DECREF(pValue);
            }
            Py_DECREF(pFunc);
        }
        Py_DECREF(pModule);
    }
    else {
        // 证明有问题,pModule为空指针了
        PyErr_Print(); // 打印错误信息
    }

    Py_Finalize(); // 关闭 Python 解释器

    return result; // 返回结果
}

int main()
{
    vector<int> result = test_cpp2py({ 100, 200, 300, 400 });

    cout << "\n上面为python脚本内的输出,下面为C++端的输出:\n";
    cout << "\n返回值为:";
    for (int item : result) {
        cout << item << ' ';
    }
    cout << endl;
}

调用结果

3375430a08cb4949ae7733bb4bea2011.png

可能存在的问题

无法重复调用函数问题

可能有些朋友会遇到连续调用两次 涉及C++调用Python 的函数,但只成功调用一次的情况,像下面这样:

int main()
{

    vector<int> result = test_cpp2py({ 100, 200, 300, 400 });
    vector<int> result_1 = test_cpp2py({ 100, 200, 300, 400 });

    cout << "\n上面为python脚本内的输出,下面为C++端的输出:\n";
    cout << "\n返回值为:";
    for (int item : result) {
        cout << item << ' ';
    }
    cout << endl;

    for (int item : result_1) {
        cout << item << ' ';
    }
    cout << endl;

}

16bae962aa354949a696c6c57d4e9b94.png

这种情况可能是由于解释器的初始化和关闭的过程赶不上函数执行的过程导致的,这个时候可以尝试把解释器初始化和关闭的过程放到函数外面(记得把原来函数里的初始化和关闭过程删掉),如下:

int main()
{
    Py_Initialize(); // 初始化 Python 解释器

    vector<int> result = test_cpp2py({ 100, 200, 300, 400 });
    vector<int> result_1 = test_cpp2py({ 100, 200, 300, 400 });

    cout << "\n上面为python脚本内的输出,下面为C++端的输出:\n";
    cout << "\n返回值为:";
    for (int item : result) {
        cout << item << ' ';
    }
    cout << endl;

    for (int item : result_1) {
        cout << item << ' ';
    }
    cout << endl;

    Py_Finalize(); // 关闭 Python 解释器
}

9023bdde13e1444fb3ba17832a1169de.png

 

搞定!!!

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值