3、Python调用C++基础:pybind11快速入门
做量化交易的朋友都知道,高频场景下性能就是生命。Python写起来爽,但跑起来慢;C++跑得快,但写起来累。怎么办?混合编程呗。
我个人习惯用pybind11来做这件事。它轻量、头文件驱动,不需要像SWIG那样生成一堆胶水代码。说白了,你写一个C++函数,加几行绑定代码,Python就能直接调用了。
3.1 为什么是pybind11?
市面上Python调用C++的方案不少:
| 方案 | 优点 | 缺点 |
|---|---|---|
| ctypes | 标准库,无需额外依赖 | 绑定繁琐,不支持C++类 |
| Cython | 性能好,可写Python风格代码 | 学习曲线陡,调试困难 |
| pybind11 | 现代C++风格,头文件驱动 | 需要C++11以上编译器 |
| Boost.Python | 功能强大 | 编译慢,依赖重 |
我在项目中用过ctypes和Cython,最后还是选了pybind11。为什么?因为它最符合直觉。你写一个C++类,用py::class_包装一下,Python端就能直接操作了。不需要写额外的接口文件,不需要学新的语法。
核心优势:pybind11是纯头文件库,只需包含头文件即可使用。它利用C++11的模板元编程技术,自动处理类型推导和转换。
3.2 环境准备
先装好pybind11。我建议用pip安装,省事:
pip install pybind11
或者从GitHub拉源码:
git clone https://github.com/pybind/pybind11.git
嗯,这里要注意:pybind11需要C++11或更高版本。如果你还在用老古董编译器,建议升级一下。我在一个老项目里踩过这个坑——GCC 4.8以下不支持C++11的std::shared_ptr,编译直接报错。
3.3 第一个C++扩展模块
我们来写一个最简单的例子。创建一个example.cpp:
#include <pybind11/pybind11.h>
namespace py = pybind11;
int add(int a, int b) {
return a + b;
}
PYBIND11_MODULE(example, m) {
m.doc() = "我的第一个C++扩展模块";
m.def("add", &add, "两个整数相加");
}
然后编译。用CMake或者直接命令行都行:
c++ -O3 -Wall -shared -std=c++11 -fPIC \
$(python3 -m pybind11 --includes) \
example.cpp -o example$(python3-config --extension-suffix)
编译成功后,你会得到一个example.cpython-xxx.so文件。在Python里直接import:
import example
print(example.add(3, 5)) # 输出 8
就这么简单。你想想看,一个C++函数,几行代码就变成了Python模块。性能上,这个add函数跑起来比纯Python快几十倍。
小技巧:编译时加上-O3优化,性能会更好。我在做回测引擎时,所有核心计算都用C++写,Python只做调度和数据处理,速度提升了近10倍。
3.4 数据类型转换
pybind11最强大的地方在于自动类型转换。它知道怎么把Python的int转成C++的int,把Python的list转成C++的std::vector。
3.4.1 基本类型:int, float, string
看个例子:
#include <pybind11/pybind11.h>
#include <string>
namespace py = pybind11;
double calculate_pnl(int trades, double avg_price, std::string symbol) {
// 模拟计算盈亏
double pnl = trades * avg_price * 0.001;
return pnl;
}
PYBIND11_MODULE(trading, m) {
m.def("calculate_pnl", &calculate_pnl,
"计算交易盈亏",
py::arg("trades"),
py::arg("avg_price"),
py::arg("symbol"));
}
Python端调用:
import trading
result = trading.calculate_pnl(100, 250.5, "BTC-USDT")
print(result) # 输出 25.05
这里有个细节:py::arg("trades")可以让你在Python里用关键字参数调用。我个人习惯把所有参数都加上名字,这样代码可读性更好。
3.4.2 容器类型:vector
高频交易中经常要处理价格序列。C++的std::vector和Python的list可以无缝转换:
#include <pybind11/pybind11.h>
#include <pybind11/stl.h> // 必须包含这个头文件
#include <vector>
#include <numeric>
namespace py = pybind11;
double calculate_vwap(const std::vector<double>& prices,
const std::vector<int>& volumes) {
if (prices.size() != volumes.size() || prices.empty()) {
return 0.0;
}
double total_value = 0.0;
int total_volume = 0;
for (size_t i = 0; i < prices.size(); ++i) {
total_value += prices[i] * volumes[i];
total_volume += volumes[i];
}
return total_volume > 0 ? total_value / total_volume : 0.0;
}
PYBIND11_MODULE(market, m) {
m.def("calculate_vwap", &calculate_vwap,
"计算成交量加权平均价(VWAP)");
}
Python端:
import market
prices = [100.5, 101.2, 102.8, 101.5]
volumes = [1000, 1500, 800, 1200]
vwap = market.calculate_vwap(prices, volumes)
print(f"VWAP: {vwap:.2f}") # 输出 VWAP: 101.42
注意:使用std::vector转换时,一定要包含pybind11/stl.h。我曾经漏掉这个头文件,结果编译通过但运行时疯狂报错,排查了半天才发现是这个问题。
3.5 类型转换的性能考量
自动转换虽然方便,但也不是免费的。每次Python调用C++函数时,参数都要从Python对象转换成C++对象,返回值也要反向转换。对于int、float这种简单类型,开销几乎可以忽略。但对于std::vector这种容器,如果数据量很大(比如几百万条行情数据),转换开销就不可忽视了。
我建议的做法是:
- 高频调用的函数,参数尽量用基本类型
- 大数据传输,考虑用内存映射或共享内存
- 如果必须传大数组,可以用NumPy的buffer protocol直接共享内存
说白了,pybind11帮你做了90%的工作,剩下10%需要你自己权衡性能和便利性。
3.6 本章知识体系
下面这张图展示了本章的核心逻辑:
这张图展示了Python调用C++的三层架构。上层是Python代码,中间是pybind11绑定层,底层是C++实现。右侧列出了pybind11支持的主要类型转换。
我的经验:刚开始用pybind11时,建议从小函数开始。先写一个简单的add函数,跑通整个编译-调用流程,再逐步增加复杂度。这样遇到问题容易定位。
好了,这一章的内容就到这里。pybind11的入门其实不难,关键是理解它的设计哲学——用最少的代码,实现Python和C++的无缝对接。下一章我们会深入探讨如何绑定C++类,以及如何处理更复杂的数据结构。
公众号:蓝海资料掘金营,微信deep3321