引言
在现代软件开发中,将高性能的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种:
- 通过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