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++对象,返回值也要反向转换。对于intfloat这种简单类型,开销几乎可以忽略。但对于std::vector这种容器,如果数据量很大(比如几百万条行情数据),转换开销就不可忽视了。

我建议的做法是:

  • 高频调用的函数,参数尽量用基本类型
  • 大数据传输,考虑用内存映射或共享内存
  • 如果必须传大数组,可以用NumPy的buffer protocol直接共享内存

说白了,pybind11帮你做了90%的工作,剩下10%需要你自己权衡性能和便利性。

3.6 本章知识体系

下面这张图展示了本章的核心逻辑:

pybind11 快速入门知识体系 Python 调用层 pybind11 绑定层 C++ 高性能实现层 自动类型转换 Python int → C++ int Python float → C++ double Python str → C++ std::string Python list → C++ std::vector Python dict → C++ std::map Python tuple → C++ std::tuple 核心:PYBIND11_MODULE 宏 + m.def() 绑定函数

这张图展示了Python调用C++的三层架构。上层是Python代码,中间是pybind11绑定层,底层是C++实现。右侧列出了pybind11支持的主要类型转换。

我的经验:刚开始用pybind11时,建议从小函数开始。先写一个简单的add函数,跑通整个编译-调用流程,再逐步增加复杂度。这样遇到问题容易定位。

好了,这一章的内容就到这里。pybind11的入门其实不难,关键是理解它的设计哲学——用最少的代码,实现Python和C++的无缝对接。下一章我们会深入探讨如何绑定C++类,以及如何处理更复杂的数据结构。


公众号:蓝海资料掘金营,微信deep3321