简明使用pybind11开发pythonc++扩展模块教程
上一篇博文写了一万多字,经过深入学习,发现使用pybind11开发pythonc++扩展模块没那么复杂。不需要visual studio或其他任何IDE工具协助,从编译器安装到环境配置全过程只需按以下步骤进行(除了最后两步顺序不能改变以外,其余步骤顺序均可以调整):
一、安装MinGW C++ 编译器
1、下载安装msys2
因为能够更方便地管理和更新 MinGW 工具链,这里推荐使用MSYS2安装。MSYS2下载地址:MSYS2。下载完了安装是一个简单的过程,我的安装路径是“C:\msys64”。打开安装路径,找到msys2.exe,双击它就打开了msys2的控制台。在msys2的控制台中输入下面的命令更新系统(当然也可以省略):
pacman -Syu
2、安装MinGW工具链
这里统一使用64位平台工具(所有工具平台必须统一)。输入下面的命令安装MinGW-w64工具链,会有一个等候过程:
pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-gdb
3、配置MinGW工具的环境变量
将 MinGW 的 bin 目录添加到系统的 PATH 环境变量中。我们这里是“C:\msys64\mingw64\bin”。
二、安装python
你可以从Python官方网站下载并安装 Python,唯一需要注意的是安装时勾选“Add Python to PATH”选项,免去自己编辑PATH环境变量的麻烦。
三、安装必要的python软件包
可以使用以下三条命令先全局安装以下软件包,以免日后开发类似项目时创建虚拟环境后还需要重复安装(当然也可以在创建项目虚拟环境后在虚拟环境中安装):
pip install setuptools
pip install wheel
pip install pybind11
安装过程中如果提示要求更新pip,按照提示信息给出的命令更新pip即可。
四、编写c++扩展模块代码
创建一个工作目录,我的是“E:\projects\cpp\cpp2python”。这次我用了在CSDN文章下面打广告的Trae来生成c++代码,向它提出了下面的要求:
生成一个c++类,符合以下条件:
1、有一个map类型的私有成员dataMap,键的数据类型为字符串,值的数据类型为字符串数组
2、有一个字符串数组类型的私有成员headers
3、有一个不带参数的默认构造函数
4、有一个接受一个字符串参数的构造函数,该参数为csv文件的路径(含文件名)
5、有一个载入csv文件的方法,参数为csv文件的路径(含文件名)。此方法读取csv文件,并根据其中的数据构造两个私有成员变量。其中第一行作为标题保存到headers中,以下各行保存到dataMap中,其中第一列作为键,后面各列作为值中的元素。文件类型不对或者读入文件失败返回空的map,并输出相关信息
6、有一个返回dataMap的方法
7、有一个返回headers的方法
8、有一个查找数据的方法,接受两个字符串参数,第一个参数为行标题,第二个参数为列标题,根据行标题和列标题在dataMap中查找对应数据,返回找到的字符串
9、有一个查找行的方法,接受一个字符串参数,以该参数为行标题在dataMap中查找一行数据并返回所找到的数据组成的字符串数组
10、有一个查找列方法,接受一个字符串参数,以该参数为列标题在dataMap中查找一列数据并返回所找到的数据组成的字符串数组
11、所有查找数据的方法如果没有查到则返回nullptr、{}或者[]
12、所有方法公开给pybind11并导出供python程序使用。c++方法采用首字母小写的驼峰命名,python方法采用下划线分隔式命名。
它生成了一个可用的代码,但是loadCSV方法对可能得csv文件解析错误只简单用文件扩展名做了一下判断,我进一步提出要求:
文件扩展名不是csv也可能能够按csv解析成功,是csv也可能解析不成功,能够将read_csv.cpp中的loadCSV方法修改成按csv格式解析文件不成功则清空dataMap和headers并输出出错信息吗?
于是它修改了loadCSV方法,最终结果如下,我没做任何实质性修改:
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <map>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
class CSVFinder {
private:
std::map<std::string, std::vector<std::string>> dataMap;
std::vector<std::string> headers;
public:
// 默认构造函数
CSVFinder() {}
// 接受 CSV 文件路径的构造函数
CSVFinder(const std::string& filePath) {
loadCSV(filePath);
}
// 载入 CSV 文件的方法
void loadCSV(const std::string& filePath) {
// 检查文件扩展名是否为 .csv
if (filePath.substr(filePath.find_last_of(".") + 1) != "csv") {
std::cerr << "文件扩展名不是 .csv,但仍尝试解析: " << filePath << std::endl;
}
std::ifstream file(filePath);
if (!file.is_open()) {
std::cerr << "无法打开文件: " << filePath << std::endl;
dataMap.clear();
headers.clear();
return;
}
std::string line;
// 读取第一行作为标题
if (!std::getline(file, line)) {
std::cerr << "无法读取文件的第一行: " << filePath << std::endl;
dataMap.clear();
headers.clear();
return;
}
std::istringstream iss(line);
std::string token;
while (std::getline(iss, token, ',')) {
headers.push_back(token);
}
if (headers.empty()) {
std::cerr << "第一行未包含有效的标题信息: " << filePath << std::endl;
dataMap.clear();
headers.clear();
return;
}
// 读取后续行
while (std::getline(file, line)) {
std::istringstream iss(line);
std::vector<std::string> values;
std::string token;
while (std::getline(iss, token, ',')) {
values.push_back(token);
}
if (values.empty()) {
std::cerr << "某行未包含有效的数据信息: " << filePath << std::endl;
dataMap.clear();
headers.clear();
return;
}
std::string key = values[0];
values.erase(values.begin());
dataMap[key] = values;
}
if (dataMap.empty()) {
std::cerr << "文件未包含有效的数据行: " << filePath << std::endl;
dataMap.clear();
headers.clear();
return;
}
file.close();
}
// 返回 dataMap 的方法
const std::map<std::string, std::vector<std::string>>& getDataMap() const {
return dataMap;
}
// 返回 headers 的方法
const std::vector<std::string>& getHeaders() const {
return headers;
}
// 查找数据的方法
std::string findData(const std::string& rowTitle, const std::string& colTitle) {
auto rowIt = dataMap.find(rowTitle);
if (rowIt != dataMap.end()) {
for (size_t i = 0; i < headers.size(); ++i) {
if (headers[i] == colTitle && i - 1 < rowIt->second.size()) {
return rowIt->second[i - 1];
}
}
}
return "";
}
// 查找行的方法
std::vector<std::string> findRow(const std::string& rowTitle) {
auto it = dataMap.find(rowTitle);
if (it != dataMap.end()) {
return it->second;
}
return {};
}
// 查找列的方法
std::vector<std::string> findColumn(const std::string& colTitle) {
std::vector<std::string> column;
size_t colIndex = 0;
for (; colIndex < headers.size(); ++colIndex) {
if (headers[colIndex] == colTitle) {
break;
}
}
if (colIndex == headers.size()) {
return {};
}
for (const auto& pair : dataMap) {
if (colIndex - 1 < pair.second.size()) {
column.push_back(pair.second[colIndex - 1]);
}
}
return column;
}
};
PYBIND11_MODULE(CSVFinder, m) {
py::class_<CSVFinder>(m, "CSVFinder")
.def(py::init<>())
.def(py::init<const std::string&>())
.def("load_csv", &CSVFinder::loadCSV)
.def("get_data_map", &CSVFinder::getDataMap)
.def("get_headers", &CSVFinder::getHeaders)
.def("find_data", &CSVFinder::findData)
.def("find_row", &CSVFinder::findRow)
.def("find_column", &CSVFinder::findColumn);
}
真的过了一把动动嘴就行的瘾,将上面的文件保存到工作目录下,文件名“read_csv.cpp”(可以随便取,扩展名不变就行。改了名的话下一步setup.py脚本中的文件名记得与这里保持一致)。
五、编写Python包构建脚本并构建c++扩展模块
1、pyhton包构建脚本:
回到工作目录,创建setup.py文件:
from setuptools import setup, Extension
import pybind11
# 设置环境了变量的话,可以用下面两行配置强行要求setuptools使用MinGW编译器
# import os
# os.environ["CC"] = "gcc" # MinGW 的 C 编译器
# os.environ["CXX"] = "g++" # MinGW 的 C++ 编译器
# 定义扩展模块
csv_module = Extension(
'CSVFinder', # 模块名称
sources=['e:/projects/cpp/cpp2python/read_csv.cpp'], # C++ 源文件路径
include_dirs=[
pybind11.get_include(), # 添加 pybind11 的头文件路径
# 如果你有其他自定义的头文件路径,可以在这里添加
# 'path/to/your/include'
],
libraries=[], # 如果你需要链接其他库,就可以在这里添加库名。例如报错没找到python312.lib,可以在这里添加python312.lib完整路径
library_dirs=[], # 如果你有自定义的库文件路径,可以在这里添加
language='c++', # 指定使用 C++ 语言
extra_compile_args=['-std=c++11'] # 编译选项
)
# 配置并设置包
setup(
name='CSVFinder',
version='1.0',
description='C++编写的Python扩展模块,用于读取CSV文件并从中查找数据。',
ext_modules=[csv_module]
)
上面脚本中csv_module变量定义中我加入了并未进行有意义的赋值的参数libraries和library_dirs。正常情况下这个项目应该能够成功构建,但万一前面的有关工具和软件包安装过程中出了问题导致找不到头文件或链接库,可以参照脚本的注释将相关库和include目录加入脚本。
2、构建c++扩展模块
在工作目录下创建Python项目的配置文件pyproject.toml,用于指定项目的构建依赖。内容如下:
[build-system]
requires = ["setuptools", "wheel", "pybind11"]
build-backend = "setuptools.build_meta"
为了不污染全局python环境,打开控制台,进入工作目录,执行以下命令创建项目的虚拟环境:
python -m venv venv
然后执行以下命令激活项目虚拟环境(windows11系统带路径执行命令要带引号)
"venv/scripts/activate"
先看看虚拟环境中是否有了必要的软件包:
pip list
如果缺少了上面必须的包(配置文件pyproject.toml中requires列表列出的包),使用pip安装。
最后执行构建命令:
pip install .
命令执行完工作目录也发生了一些变化:
生成的扩展包文件名可以修改,可以直接拷贝到其他python项目中使用,也可以在其他python项目中用pip install再次构建安装,但路径不能够再是一个点(“.”),而要用工作目录的路径“E:\projects\cpp\cpp2python”。强制使用mingw编译器时,也可以用下面两条命令完成扩展包的构建:
python setup.py build
python setup.py install
六、测试c++扩展
在工作目录下编写test.py测试:
finder = CSVFinder()
finder.load_csv("E:/projects/ziweidoushu/csv/destiny_type.csv")
# print(finder.get_data_map())
print(finder.get_headers())
print(finder.find_data('丁', '亥'))
print(finder.find_data('丁', '子') == '')
f = finder.find_data('子', '子')
print(f'f:{f is None}')
print(finder.get_row('丁'))
print(finder.get_column('子'))
运行后发现模块能够加载成功,但是好几个方法的运行结果不如预期☺。这是c++扩展模块代码的问题,而不是构建过程的问题。看来Trae还要努力啊(据说是介入的deepseek的接口?)!昨天弄的微软copilot接入的gpt-4o,生成的代码运行起来结果是符合预期的啊。