如何使用Python调用C++代码?

引言

在现代软件开发中,将高性能的C++代码与灵活易用的Python相结合是一种常见的需求。网上已有大量关于如何使用Python调用C++的文章或视频,本文作为自己的学pybind11笔记吧。

系统及环境

  • 操作系统: Windows
  • 开发工具: Visual Studio 2022 (推荐使用最新版本,支持GitHub Copilot)
  • Python版本: 3.12.9 (建议使用3.6+版本)
  • 构建工具: CMake

安装pybind11

Pybind11 是一个轻量级、只包含头文件的库,用于 Python 和 C++ 之间接口转换,可以为现有的 C++ 代码创建 Python 接口绑定。安装方式有2种:

    1. 通过pip安装(推荐):
pip3 install pybind11
  • 2.源代码安装:
git clone https://github.com/pybind/pybind11

项目结构

在Visual Studio 2022里创建空白项目,名称为pybind11_demo。创建如下的项目结构:

pybind11_demo/
├── CMakeLists.txt        # 项目构建配置文件
├── include/
│   └── pet.h             # C++头文件
├── src/
│   ├── pet.cpp           # C++实现文件
│   └── binding.cpp       # Python绑定代码
└── test/
    └── test_module.py    # Python测试脚本

C++代码

pet.h中,定义一个完整的宠物类,用于接下来展示pybind11的绑定能力。

#include <string>
#include <iostream>

struct Pet {

    enum Kind {
        Dog = 0,
        Cat
    };

    Pet(const std::string& name, Kind type);

    void setName(const std::string& name_);
    const std::string& getName() const;

    void setAge(int age_);
    int getAge() const;

    virtual std::string sing() const;
    virtual void play() const;

    std::string name;
    Kind type;
    int age;
};

C++实现代码pet.cpp

#include "pet.h"

Pet::Pet(const std::string& name, Kind type) : name(name), type(type), age(0) {}

void Pet::setName(const std::string& name_) {
    name = name_;
}

const std::string& Pet::getName() const {
    return name;
}

void Pet::setAge(int age_) {
    age = age_;
}

int Pet::getAge() const {
    return age;
}

std::string Pet::sing() const {
    return "Default song";
}

void Pet::play() const {
    std::cout << name << " is playing." << std::endl;
}

Python绑定实现

binding.cpp中,我们使用pybind11创建Python绑定::

#include <pybind11/pybind11.h>
#include "pet.h"

namespace py = pybind11;

// 模块初始化函数
PYBIND11_MODULE(pet_module, m) {
    m.doc() = "Python bindings for Pet class";  // 模块文档
    
    // 绑定Pet类
    py::class_<Pet>(m, "Pet")
        // 构造函数绑定
        .def(py::init<const std::string&, Pet::Kind>(), 
             py::arg("name"), 
             py::arg("kind") = Pet::Kind::Dog,  // 默认参数
             "Construct a Pet with name and kind")
             
        // 方法绑定
        .def("set_name", &Pet::setName, py::arg("name"), "Set the pet's name")
        .def("get_name", &Pet::getName, "Get the pet's name")
        .def("set_age", &Pet::setAge, py::arg("age"), "Set the pet's age")
        .def("get_age", &Pet::getAge, "Get the pet's age")
        .def("sing", &Pet::sing, "Make the pet sing")
        .def("play", &Pet::play, "Make the pet play")
        
        // 属性绑定
        .def_readwrite("name", &Pet::name)
        .def_readwrite("age", &Pet::age)
        .def_readwrite("type", &Pet::type)
        
        // 字符串表示
        .def("__repr__", [](const Pet &p) {
            return "<Pet name='" + p.name + "'>";
        });

    // 枚举绑定
    py::enum_<Pet::Kind>(m, "Kind")
        .value("Dog", Pet::Kind::Dog)
        .value("Cat", Pet::Kind::Cat)
        .export_values();
};

CMake构建配置

CMakeLists.txt是项目的构建核心,在CMakeLists.txt 文件里:

# CMake 最低版本和项目名称
cmake_minimum_required(VERSION 3.17...3.24)

# 项目名称,根据实际情况定义
project(demo) 

# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 自定义python模块名称
 set(PY_MODULE_NAME "pet_module")  

# 包含头文件路径,目的是src里的cpp文件包含头文件时不需要目录
include_directories(${PROJECT_SOURCE_DIR}/include)

# 查找 Python 相关组件
find_package(Python COMPONENTS Interpreter Development REQUIRED)

# 查找 pybind11 目录
# 正常情况下是不需要执行这些命令,直接跳到find_package(pybind11 CONFIG REQUIRED)。在我的电脑上无法直接找到pybind11, 故用此方法。
execute_process(
    COMMAND ${Python_EXECUTABLE} -m pybind11 --cmakedir
    OUTPUT_VARIABLE PYBIND11_CMAKE_DIR
    OUTPUT_STRIP_TRAILING_WHITESPACE
)
message(STATUS "PYBIND11_CMAKE_DIR: ${PYBIND11_CMAKE_DIR}")

# 去除路径开头和结尾的引号
string(REGEX REPLACE "^\"|\"$" "" PYBIND11_CMAKE_DIR_FIXED "${PYBIND11_CMAKE_DIR}")
set(pybind11_DIR "${PYBIND11_CMAKE_DIR_FIXED}")

# 查找 pybind11
find_package(pybind11 CONFIG REQUIRED)

# 自动获取 src 目录下的所有 .cpp 文件 (可能有多个源文件需要打包)
file(GLOB SRC_FILES "${PROJECT_SOURCE_DIR}/src/*.cpp")

# 添加 pybind11 绑定模块(使用用户定义的名称)
pybind11_add_module(${PY_MODULE_NAME}  ${SRC_FILES})

# 在构建后复制 .pyd到测试目录里,方便在import 到py文件里
add_custom_command(TARGET ${PY_MODULE_NAME} POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy
        $<TARGET_FILE:${PY_MODULE_NAME}>
        ${PROJECT_SOURCE_DIR}/test/$<TARGET_FILE_NAME:${PY_MODULE_NAME}>
)

构建与测试

构建过程使用标准的CMake流程:

mkdir build # 在项目根目录下创建
cd build
cmake ..    #没有错误,再输入下面的命令
cmake --build . --config Debug

然后你会发现在项目根目录下多出一个build文件夹。另外生成的动态库文件pet_module.cp312-win_amd64.pyd也自动复制到test文件夹里。

图1 编译后的文件结构

Python端使用

test_module.py中,展示完整的Python端使用示例:

from pet_module import Pet, Kind

class MyPet(Pet):
    """扩展C++类的Python子类"""
    
    def __init__(self, name, kind=Kind.Dog):
        super().__init__(name, kind)
        self._tricks = []  # 添加Python特有属性
        
    def add_trick(self, trick):
        """添加Python特有方法"""
        self._tricks.append(trick)
        return f"{self.name} learned {trick}"
        
    def sing(self):
        """重写C++虚函数"""
        return f"{self.name} sings: {' '.join(self._tricks)}" if self._tricks else super().sing()
        
    def play(self):
        """扩展C++方法"""
        super().play()  # 调用父类实现
        return f"{self.name} is playing with {len(self._tricks)} tricks"

if __name__ == '__main__':
    # 测试基本功能
    pet = MyPet("Buddy")
    print(pet)           # <Pet name='Buddy'>
    print(pet.type)      # Kind.Dog (默认值)
    
    # 测试方法调用
    print(pet.add_trick("roll over"))  # Buddy learned roll over
    print(pet.add_trick("fetch"))      # Buddy learned fetch
    
    # 测试重写方法
    print(pet.sing())    # Buddy sings: roll over fetch
    print(pet.play())    # Buddy is playing with 2 tricks
    
    # 测试属性访问
    pet.name = "Max"
    pet.age = 3
    print(f"{pet.name} is {pet.age} years old")  # Max is 3 years old
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容