
你是否也有过这样的纠结——用 Python 写算法原型又快又爽,可一到生产环境,for 循环跑得比蜗牛还慢?又或者团队用 C++ 写了一套高性能核心库,但上层应用和测试脚本只能用 Python 来写,两边怎么"握手"成了头疼的问题?
如果你正面临这些痛点,这篇文章就是为你准备的。我们会从两个方向系统梳理 C++ 与 Python 混合编程的主流方案:C++ 调用 Python(嵌入式 Embedding)和 Python 调用 C++(扩展式 Extending),涵盖从底层 C-API 到现代 pybind11 的完整技术栈,附带可运行的代码示例和性能对比。
- - -
Python 以其简洁的语法和丰富的生态系统成为算法验证、数据分析和 AI 领域的首选语言。然而在计算密集型场景(图像处理、数值仿真、大规模矩阵运算)下,Python 的解释执行特性会成为性能瓶颈。C++ 拥有接近硬件的执行效率,却缺少 Python 那样灵活的胶水能力。
混合编程的核心思路是:让每种语言做自己最擅长的事。在 Python 平台上完成算法设计、流程编排和快速验证,将性能瓶颈模块用 C++ 重新实现并编译为动态链接库(.so / .pyd),再通过 Python import 调用实现本地加速。反过来,在 C++ 主控程序中嵌入 Python 解释器,可以复用 Python 生态中丰富的开源库(NumPy、PyTorch、pandas 等),实现功能扩展和热更新。

▲ 混合编程双向架构:上层 Python 调用 C++ 加速,下层 C++ 嵌入 Python 扩展
在深入代码之前,我们先建立全局认识。C++ 与 Python 的互操作方案可以按两个维度划分:调用方向(谁调谁)和抽象层次(底层是C-API 还是其他模块接口)。
| Python/C API | ||||
| pybind11 | ||||
| ctypes | ||||
| Cython | ||||
| SWIG |
2025 年后的推荐:对于大多数新项目,pybind11 是最优选择。它同时支持双向调用,是纯头文件库(header-only),不需要 Boost 依赖,并且拥有活跃的社区维护。下面我们将按两个方向分别展开。
pybind11 是一个轻量级的纯头文件库,专门用于 C++ 与 Python 之间的接口转换。它通过 C++ 编译时的类型推断,最大程度地减少了传统扩展 Python 模块时繁杂的样板代码。相比 Boost.Python 的庞大和复杂,pybind11 十分轻量且上手极快。
来看一个完整的例子——我们将用 C++ 编写一个模块,暴露整数相加和 NumPy 数组相加两个函数给 Python:
#include<pybind11/pybind11.h>#include<pybind11/numpy.h>#include<omp.h>namespace py = pybind11;// 简单的整数相加intadd(int i = 1, int j = 2){return i + j;}// NumPy 数组相加(带 OpenMP 并行加速)py::array_t<double> add_c(py::array_t<double> arr1,py::array_t<double> arr2) {py::buffer_info buf1 = arr1.request(),buf2 = arr2.request();if (buf1.shape != buf2.shape)throw std::runtime_error("Input shapes must match");auto result = py::array_t<double>(buf1);py::buffer_info buf3 = result.request();double* ptr1 = (double*)buf1.ptr;double* ptr2 = (double*)buf2.ptr;double* ptr3 = (double*)buf3.ptr;#pragma omp parallel forfor (ssize_t idx = 0; idx < buf1.size; idx++)ptr3[idx] = ptr1[idx] + ptr2[idx];return result;}// 模块绑定入口PYBIND11_MODULE(example1, m) {m.doc() = "pybind11 example plugin";m.def("add", &add, "Add two integers",py::arg("i") = 1, py::arg("j") = 2);m.def("add_c", &add_c, "Add two arrays");}
编译生成的 .pyd(Windows)或 .so(Linux)文件放到 Python 运行目录下,就可以像普通模块一样使用:
import example1import numpy as np# 整数相加print(example1.add(10, 20)) # 输出: 30# NumPy 数组相加(底层 C++ + OpenMP 并行)a = np.ones((3, 4, 5))b = np.ones((3, 4, 5))result = example1.add_c(a, b)print(result.shape) # (3, 4, 5)
性能实测:使用 OpenMP 并行加速的 C++ 数组运算,实测速度可达纯 NumPy 实现的 90% 以上,在大规模数据场景下优势更为显著。
理解 NumPy 数组在 pybind11 中的工作方式至关重要。pybind11 通过 py::array_t<T> 类型与 numpy.ndarray 互转,而底层的 request() 函数会将数组解析为 py::buffer_info 结构体,提供对内部数据的快速直接访问:
struct buffer_info {void* ptr; // 指向数组数据的指针py::ssize_t itemsize; // 单个元素大小(字节)std::string format; // 元素格式描述py::ssize_t ndim; // 数组维度数std::vector<py::ssize_t> shape; // 各维度大小std::vector<py::ssize_t> strides; // 各维度相邻元素字节间隔};
这里需要特别注意 strides 字段——它描述的是每个维度上相邻元素在内存中的字节间隔。对于行优先存储的二维 float 数组(rows × cols),行方向的 stride 是 sizeof(float) * cols,列方向的 stride 是 sizeof(float)。理解 strides 是正确解析和重包装多维数组的关键。
不论数组是多少维度,底层始终是一块一维的连续内存,因此可以用一个大循环直接遍历所有元素。这也是为什么用 buffer.size(元素总数)做循环上限如此方便的原因。
当需要丰富的线性代数操作时,pybind11 同样提供了对 Eigen 矩阵库的直接转换支持(#include <pybind11/eigen.h>),函数参数和返回值可以直接使用 Eigen::Matrix 类型。但这里有一个重要的性能陷阱:
注意:默认情况下,pybind11 会将 NumPy 数组复制一份到临时 Eigen 变量再调用 C++ 函数,返回时再复制回来。对于大矩阵,这会显著拖慢运行速度!
解决方案是使用 Eigen::Ref<MatrixType> 实现引用传递,避免不必要的内存拷贝。另外要注意 NumPy(行优先)和 Eigen(列优先)默认存储顺序不同,需要用 Eigen::RowMajor 参数显式指定为行优先,否则转换时可能发生内存错误。
Eigen::Matrix 和 Eigen::Array 仅支持二维,处理三维及以上的高维数组需要借助 Eigen 实验模块中的 Eigen::Tensor(位于 <unsupported/Eigen/CXX11/Tensor>)。pybind11 没有直接转换 Tensor 类型的接口,需要通过 py::buffer_info 手动重构:
#include<unsupported/Eigen/CXX11/Tensor>template<class T>py::array_t<T> eigenTensor(py::array_t<T> inArray) {py::buffer_info bi = inArray.request();T* data = static_cast<T*>(bi.ptr);auto shape = bi.shape;// 用 TensorMap 包装为 3D Tensor(避免拷贝)Eigen::TensorMap<Eigen::Tensor<T, 3>>in_tensor(data, shape[0], shape[1], shape[2]);// 在这里对 Tensor 进行操作...Eigen::Tensor<T, 3> out_tensor(shape[0], shape[1], shape[2]);// ... 计算逻辑 ...// 返回为 numpy 数组return py::array_t<T>(shape,{shape[1]*shape[2]*sizeof(T),shape[2]*sizeof(T),sizeof(T)},out_tensor.data());}
需要注意的是 Eigen::Tensor 目前仍属于非官方主干模块,文档不够完善,特征方法较少。如果你的项目对高维数组操作有较高需求,建议直接在 C++ 中使用 py::array_t 的 buffer 机制,或者回到 Python 侧完成高维操作。
如果你只需要快速调用一个已有的 C 动态链接库,不想引入额外的编译依赖,ctypes 是最简单的选择——它是 Python 标准库的一部分,无需安装任何第三方包。
// C++ 侧:math_lib.cppextern "C" {intadd(int a, int b) { return a + b; }doublemultiply(double a, double b) { return a * b; }}
# Python 侧import ctypeslib = ctypes.CDLL('./libmath.so')lib.add.argtypes = [ctypes.c_int, ctypes.c_int]lib.add.restype = ctypes.c_intresult = lib.add(3, 4) # 7
ctypes 的局限在于必须使用 extern "C" 导出的C 语言函数接口,无法直接绑定 C++ 的类、模板和重载函数,类型转换需要手动声明。但在对接已有 C 库(如系统 API、第三方 SDK)时,它是最省事的方案。
Cython 是一种 Python 超集语言(.pyx 文件),编译后生成 C 扩展模块。它的独特之处在于可以用类似 Python 的语法写出接近原生 C 速度的代码,特别适合数值计算和科学计算场景。
# calc.pyx# distutils: language = c++from libcpp.vector cimport vectorcdef extern from "calculator.h":cdef cppclass Calculator:Calculator() except +double add(double a, double b)cdef class PyCalculator:cdef Calculator* thisptrdef __cinit__(self):self.thisptr = new Calculator()def __dealloc__(self):del self.thisptrdef add(self, double a, double b):return self.thisptr->add(a, b)
Cython 的 with nogil: 块可以释放 GIL,让多线程真正并行执行 C 代码,这是其他方案不容易做到的。代价是需要学习额外的 Cython 语法和多一步编译流程。
有时候方向是反过来的:你有一个 C++ 主控程序(游戏引擎、仿真平台、嵌入式系统),但希望利用 Python 生态中丰富的开源库来做数据处理、机器学习推理或更新脚本。这就需要在 C++ 中嵌入 Python 解释器。
Python 官方提供的 C-API 是最底层的嵌入接口,每一个 Python 对象都通过 PyObject* 指针来操作。虽然代码写起来繁琐,但拥有完全的控制能力。来看一个完整的嵌入示例:
#define PY_SSIZE_T_CLEAN#include<Python.h>intmain(int argc, char* argv[]){// 1. 初始化 Python 解释器Py_Initialize();// 2. 导入 Python 模块PyObject* pName = PyUnicode_DecodeFSDefault(argv[1]);PyObject* pModule = PyImport_Import(pName);Py_DECREF(pName);if (pModule != NULL) {// 3. 获取函数对象PyObject* pFunc = PyObject_GetAttrString(pModule, argv[2]);if (pFunc && PyCallable_Check(pFunc)) {// 4. 构建参数元组PyObject* pArgs = PyTuple_New(argc - 3);for (int i = 0; i < argc - 3; i++) {PyObject* val = PyLong_FromLong(atoi(argv[i+3]));PyTuple_SetItem(pArgs, i, val); // steals ref}// 5. 调用函数PyObject* pValue = PyObject_CallObject(pFunc, pArgs);Py_DECREF(pArgs);if (pValue != NULL) {printf("Result: %ld\n", PyLong_AsLong(pValue));Py_DECREF(pValue);}Py_XDECREF(pFunc);}Py_DECREF(pModule);}if (PyErr_Occurred()) PyErr_Print();Py_FinalizeEx();return 0;}
引用计数是 C-API 的核心难点。每个 PyObject* 都有引用计数,忘记 Py_DECREF 会导致内存泄漏,多减一次会导致崩溃。返回新引用(New Reference)的函数需要你在用完后释放,返回借用引用(Borrowed Reference)的函数不需要。
pybind11 提供了 embed 模块,用 RAII 模式将解释器的初始化/销毁封装在 py::scoped_interpreter 中,大幅降低了 C-API 的复杂性:
#include<pybind11/embed.h>namespace py = pybind11;intmain(){// 解释器生命周期自动管理py::scoped_interpreter guard{};// 直接使用 Python 标准库py::module_ sys = py::module_::import("sys");py::print("Python version:", sys.attr("version"));// 执行 Python 代码并获取返回值py::object result = py::eval("sum(range(100))");int total = result.cast<int>();py::print("Sum:", total); // 4950return 0;}
更强大的是,你可以在 C++ 中直接 import NumPy 等 Python 库并调用其函数,实现真正意义上的"混合编程":
#include<pybind11/embed.h>#include<pybind11/numpy.h>namespace py = pybind11;// 等价于 Python 中的 import numpy as nppy::object np = py::module_::import("numpy");voiddemo(py::array_t<double> arr){// 调用 ndarray 的方法py::object max_val = arr.attr("max")();py::object shape = arr.attr("shape");// 调用 numpy 函数py::array_t<double> ones =np.attr("ones")(std::make_tuple(3,4), "dtype"_a="double");// 使用 py::eval 执行表达式auto local = py::dict();local["arr"] = arr;auto mask = py::eval("(arr > 5)", local);py::print("Max:", max_val, "Shape:", shape);}
这里 np.attr("ones")(...) 等价于 Python 中的 np.ones(...),"dtype"_a 是 pybind11 的语法糖,用来传递 Python 的 keyword 参数。通过这种方式,你可以在 C++ 中几乎完整地复用 Python 的模块和函数。
在嵌入场景中,你还可以把 C++ 函数注册为 Python 的"内置模块",让 Python 代码可以直接 import 使用,就像标准库一样:
// 定义内置模块(全局作用域)PYBIND11_EMBEDDED_MODULE(fast_calc, m) {m.def("add", [](int i, int j) { return i + j; });m.def("multiply", [](double a, double b) {return a * b;});}intmain(){py::scoped_interpreter guard{};// Python 侧可以直接 importpy::exec("import fast_calc\n""print(fast_calc.add(3, 4))" # 输出 7);return 0;}
这种双向互通的能力使得 pybind11 成为混合编程的最佳桥梁——C++ 可以调用 Python 的开源生态,Python 脚本又可以反过来调用 C++ 注册的高性能函数,形成工具闭环。
Windows 下的环境配置相对繁琐,这里基于 Visual Studio 2019 + Miniconda3 + pybind11 给出一份可复用的配置记录。
运行环境:Win10 / VS2019 x64 / Miniconda3 with Python 3.7+
第一步:安装 pybind11pybind11 是 header-only 的,直接解压即可。下载后设置环境变量 PYBIND11 指向解压目录(如 C:\VS_Lib\pybind11)。
第二步:配置 Visual Studio 项目属性建议新建一个 pybind11.props 属性页方便复用:
▪ 输出类型:目标扩展名 .pyd,配置类型为"动态库(.dll)"▪ 包含目录:$(PYBIND11)\include 和 Miniconda3\include▪ 链接器附加库目录:Miniconda3\libs▪ 附加依赖项:python3.lib; python37.lib
Windows 嵌入 Python 的特殊注意:如果需要 C++ 调用 Python 解释器(py::scoped_interpreter),必须额外设置两个系统环境变量 PYTHONHOME 和 PYTHONPATH,否则会报 initfsencoding 错误。这是 Windows 下的已知问题。
编译成功后,输出的 .pyd 文件拷贝到 Python 运行同级目录下即可通过 import 导入使用。
对于新项目,推荐使用 CMake 作为 C++ 侧的构建系统,配合 pybind11 的 CMake 集成可以非常简洁地配置:
cmake_minimum_required(VERSION 3.15)project(my_project)set(CMAKE_CXX_STANDARD 17)# 自动下载 pybind11include(FetchContent)FetchContent_Declare(pybind11GIT_REPOSITORY https://github.com/pybind/pybind11.gitGIT_TAG v2.13)FetchContent_MakeAvailable(pybind11)# 生成 Python 扩展模块pybind11_add_module(mylib src/bindings.cpp)# 生成嵌入 Python 的 C++ 可执行文件add_executable(my_app src/main.cpp)target_link_libraries(my_app PRIVATE pybind11::embed)
对于 Python 侧的打包分发,可以结合 scikit-build-core 使用 pyproject.toml 标准配置,实现 pip install 一键安装:
# pyproject.toml[build-system]requires = ["scikit-build-core", "pybind11"]build-backend = "scikit_build_core.build"[project]name = "mylib"version = "0.1.0"
经过多个项目的实践,以下是混合编程中最容易踩坑的几个要点:
1. 减少边界跨越次数Python 与 C++ 之间的每次调用都有转换开销。避免在 Python 的 for 循环中频繁调用 C++ 函数,而是将数据批量传入、在 C++ 侧一次性完成计算。开销在数据转换而非"计算"。
2. 善用零拷贝传递使用 py::array_t 的 buffer 机制或 Eigen::Ref / Eigen::Map 实现引用传递,避免大数组的多余内存拷贝。
3. GIL 管理不可忽视在 C++ 长耗时计算中,使用 py::call_guard<py::gil_scoped_release>() 释放 GIL,让 Python 的其他线程能继续运行。在多线程 C++ 代码中回调 Python 时,需要用 py::gil_scoped_acquire 重新获取 GIL。
4. 异常处理要完整在嵌入场景中,始终用 try-catch(py::error_already_set&) 捕获 Python 异常。在扩展模块中,让 pybind11 自动将 C++ 异常翻译为 Python 异常。
5. 关注 Python 3.13+ 无 GIL 模式Python 3.13 引入了实验性的 free-threaded 构建(PEP 703)。如果你编写的扩展模块未来需要支持无 GIL 环境,需要审查代码中的全局状态、缓存和借用引用(Borrowed Reference),使用 PyList_GetItemRef() 等线程安全替代方案。建议现在就开始用 python3.13t 做兼容性测试。

C++ 与 Python 的混合编程已经从早期的"痛苦配置"演进到了"优雅协作"。pybind11 作为现代方案的核心,以其轻量、header-only、双向支持的特性,成为绝大多数新项目的首选。
选择合适的方案其实很简单:如果你要暴露 C++ 库给 Python 使用,选 pybind11;如果你要写出类似 Python 语法但达到 C 速度的代码,选 Cython;如果你需要同时生成 Python、Java、C# 等多语言绑定,选 SWIG;如果你只需要快速调用一个已有的 C 动态库,选 ctypes。
混合编程的精髓不在于技术的堆砌,而在于让每种语言回归它最擅长的领域。Python 负责灵活编排和快速迭代,C++ 负责极致性能和底层控制——两者通过精心设计的接口层无缝协作,这才是工程实践中最优雅的解法。
- END -
参考资料:[1] pybind11 官方文档:pybind11.readthedocs.io[2] Python/C API Reference Guide(Python 3.10):docs.python.org/3/c-api/[3] Cython 官方文档:cython.readthedocs.io