您现在的位置是:首页 >技术杂谈 >用C/C++扩展Python,包括类、方法、变量(Windows环境)网站首页技术杂谈

用C/C++扩展Python,包括类、方法、变量(Windows环境)

Eliza_Her 2024-10-15 00:01:03
简介用C/C++扩展Python,包括类、方法、变量(Windows环境)

环境和工具:Windows10、Visual Studio 2019、Python3.10、PowerShell、pip

内容:分别展示了使用Python/C API和pybind11,在Python中调用C++的类、方法、变量的过程。

注意:过程就是把C++文件封装成Python包在Python中导入包并使用。也就是说,该方法不支持C++类、方法、变量的动态修改,每次对包进行修改,都需要重新编译C++代码。

阅前提醒:本文不包含对于代码写法的解释,只提供了能跑通的代码、使用步骤。如果对代码写法感兴趣,建议搜索词:“Python 扩展 C++”。

目录

一、使用Python/C API

1.1 配置环境

1.2 被打包的代码

1.3 打包工具代码-C++ 

1.4 打包工具代码-setup.py

1.5 打包过程

1.6 使用包

二、使用pybind11

2.1 配置环境

2.2  CMakeLists

2.3 C++代码

2.4 Python代码


一、使用Python/C API

1.1 配置环境

在Visual Studio中新建空项目BindingTest。

配置CPython环境。配置方法见博客:C++使用Python/C API_Eliza_Her的博客-CSDN博客

1.2 被打包的代码

编写myclass.cpp,这里是要被打包的代码,包括下面三个内容:

  1. MyClass类
  2. my_variable变量
  3. update_variable方法
#include <iostream>
#include <string>
#include <Python.h>

// C++代码
class MyClass {
public:
    void printMessage(const std::string& message) {
        std::cout << "Message: " << message << std::endl;
    }
};

int my_variable = 42;
void update_variable(int new_value) {
    my_variable = new_value;
}

1.3 打包工具代码-C++ 

编写myClassPy.cpp,这是打包的工具代码,该代码为包定义了四个方法:

  1. 实例化MyClass类
  2. 调用MyClass的printMessage方法
  3. 读取my_variable
  4. 更新my_variable(注意,这个更新只在Python环境下有效,不能对C++环境中的my_variable造成影响)
#include <Python.h>
#include "myclass.cpp"
#include "valueTest.cpp"

static PyObject* create_instance(PyObject* self, PyObject* args) {
    MyClass* myclass = new MyClass();
    return PyCapsule_New(myclass, "myclass", NULL);
}

static PyObject* print_message(PyObject* self, PyObject* args) {
    PyObject* capsule;
    const char* message;
    if (!PyArg_ParseTuple(args, "Os", &capsule, &message)) {
        return nullptr;
    }

    const char* name = PyCapsule_GetName(capsule);
    if (name == nullptr) {
        return nullptr;
    }
    MyClass* instance = (MyClass*)PyCapsule_GetPointer(capsule, name);
    if (instance == nullptr) {
        return nullptr;
    }

    instance->printMessage(message);
    Py_RETURN_NONE;
}

static PyObject* get_variable(PyObject* self, PyObject* args) {
    return PyLong_FromLong(my_variable);
}

static PyObject* set_variable(PyObject* self, PyObject* args) {
    int new_value;
    if (!PyArg_ParseTuple(args, "i", &new_value)) {
        return NULL;
    }

    update_variable(new_value);

    Py_RETURN_NONE;
}

static PyMethodDef module_methods[] = {
    {"create_instance", create_instance, METH_NOARGS, "Create an instance of MyClass."},
    {"print_message", print_message, METH_VARARGS, "Print a message using MyClass."},
    {"get_variable", get_variable, METH_NOARGS, "Get the value of the variable."},
    {"set_variable", set_variable, METH_VARARGS, "Set the value of the variable."},
    {NULL, NULL, 0, NULL}
};

static struct PyModuleDef module_definition = {
    PyModuleDef_HEAD_INIT,
    "myclass",
    "A module that exposes a C++ class.",
    -1,
    module_methods
};

PyMODINIT_FUNC PyInit_myclass(void) {
    return PyModule_Create(&module_definition);
}

1.4 打包工具代码-setup.py

 新建setup.py,输入以下内容,其中version和author可省略:

from distutils.core import setup,Extension

MOD = 'myclass'
setup(name=MOD,
	version = '1.0',
	author = 'Eliza',
	ext_modules=[Extension(MOD,sources=['myClassPy.cpp'])])

1.5 打包过程

在管理员模式下运行PowerShell,运行以下语句:

python setup.py build
python setup.py install

如果没有报错,则可以看到,一开始的代码已经打包成了Python包,包名是myclass。

1.6 使用包

在python文件中输入以下代码并运行,以测试包的功能:

import myclass

value = myclass.get_variable()
print("first use value: %d" %value)

myclass.set_variable(233)
value = myclass.get_variable()
print("second use value: %d" %value)

instance = myclass.create_instance()
myclass.print_message(instance, "hello")

输出:

first use value: 42

second use value: 233

Message: hello

二、使用pybind11

官方文档对于基础用法给出了详细的示例,比本文列出的更广泛:

First steps - pybind11 documentation

Object-oriented code - pybind11 documentation

2.1 配置环境

与Linux环境不同,Windows环境下的pybind11的环境配置方法见该文章:【pybind11入门】Windows下为Python创建C++扩展|安装、CMake编译、调用 - 知乎 (zhihu.com)

官方给出的配置方法:Build systems - pybind11 documentation 

我采用了CMake配置法Windows环境下,使用pybind11为Python扩展C++分为以下步骤(具体操作参考链接):

  1. 用pip安装pytest
    pip install pytest
  2.  从github上获取pybind11,并通过cmake进行编译(最后一步可能报错,最后一步不能运行也没关系,只要不是严重错误就可以忽视)
    git clone https://github.com/pybind/pybind11
    cd pybind11
    mkdir build
    cd build
    cmake ..
    cmake --build . --config Release --target check
  3. 把pybind11复制到项目文件夹下
  4. 编写C++代码(见2.3节
  5. 编写CMakeLists(见2.2节),通过cmake编译项目
    mkdir build
    cd build
    cmake ..
  6. 用VS打开 项目名.vcxproj 文件,点击生成→生成解决方案
  7. build/Debug/ 目录下(注意:不是 Debug/ 目录下)找到 项目名.xxx.pyd 文件,把文件名中间的 .xxx 删去,将文件复制到python代码相同目录下
  8. 编写python代码,通过 import 项目名 导入包(见2.4节
  9. 运行python代码

注意:修改C++代码之后,从第6步开始执行,以获取新的pyd文件 。

2.2 CMakeLists

新建项目pybindTest,在项目根目录下新建CMakeLists.txt,内容如下:

cmake_minimum_required(VERSION 3.4)
project(pybindTest LANGUAGES CXX)

add_subdirectory(pybind11)
pybind11_add_module(pybindTest my_module.cpp)

2.3 C++代码

my_module.cpp,涉及如下操作:

  1. 全局变量
  2. 函数(支持默认参数)
  3. 类(支持带参方法、修改属性【即动态参数】)
#include <pybind11/pybind11.h>
#include <iostream>
#include <string>

namespace py = pybind11;

int my_variable = 42;

int add(int x, int y) {
    return x + y;
}

class MyClass {
public:
    MyClass(int x, int y) :m_x(x), m_y(y) {}
    void printMessage(const std::string& message) {
        std::cout << "Message: " << message << std::endl;
    }
    void printLocation() {
        std::cout << "Location: (" << m_x << ", " << m_y << ")" << std::endl;
    }
    std::string className = "Eliza";
private:
    int m_x, m_y;
};

PYBIND11_MODULE(pybindTest, m) {
    m.doc() = "test pybind11";
    //定义变量
    m.attr("name") = "Eliza";
    m.attr("age") = 108;

    //临时定义函数内容
    m.def("get_variable", []() {
        return my_variable;
        }, "get variable");

    m.def("set_variable", [](int value) {
        my_variable = value;
        }, "set variable");

    //事先定义函数内容,通过py::arg设置参数的默认值
    //只有前两个参数必填,后3个参数可不填
    m.def("add", &add, "A function that adds two numbers",
        py::arg("i") = 1, py::arg("j") = 2);

    //类,及其带参方法init和printMessage、动态参数name
    py::class_<MyClass>(m, "MyClass", py::dynamic_attr())
        .def(py::init<int, int>())
        .def("printMessage", &MyClass::printMessage)
        .def("printLocation", &MyClass::printLocation)
        .def_readwrite("name", &MyClass::className);//另一种写法:def_property()
}

2.4 Python代码

import pybindTest

print("name: ", pybindTest.name)
print("age: ", pybindTest.age)

value = pybindTest.get_variable()
print("Variable value: ", value)

pybindTest.set_variable(100)
new_value = pybindTest.get_variable()
print("New variable value: ", new_value)

addNum = pybindTest.add()
print("Default add result: ", addNum)
addNum = pybindTest.add(3, 4)
print("New add result: ", addNum)

myclass = pybindTest.MyClass(1, 3)
myclass.printLocation()
print("myclass name: ", myclass.name)
myclass.name = "Sera"
print("new myclass name: ", myclass.name)
myclass.printMessage("Hello")

输出:

name:  Eliza
age:  108
Variable value:  42
New variable value:  100
Default add result:  3
New add result:  7
Location: (1, 3)
myclass name:  Eliza
new myclass name:  Sera
Message: Hello

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。