本章介紹 pybind11,一個輕量級的 header-only C++ 函式庫,用於建立 Python 綁定。

本章目標

學完本章後,你將能夠:

  1. 理解 pybind11 的設計哲學
  2. 建立函式和類別綁定
  3. 處理 NumPy 陣列

【原理層】pybind11 的設計哲學

為什麼需要 pybind11?

傳統的 Python C API 非常繁瑣:

 1// 傳統 Python C API
 2static PyObject* add(PyObject* self, PyObject* args) {
 3    int a, b;
 4    if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
 5        return NULL;
 6    }
 7    return PyLong_FromLong(a + b);
 8}
 9
10static PyMethodDef methods[] = {
11    {"add", add, METH_VARARGS, "Add two integers"},
12    {NULL, NULL, 0, NULL}
13};
14
15static struct PyModuleDef module = {
16    PyModuleDef_HEAD_INIT,
17    "example",
18    NULL,
19    -1,
20    methods
21};
22
23PyMODINIT_FUNC PyInit_example(void) {
24    return PyModule_Create(&module);
25}

pybind11 讓這變得簡單:

 1// pybind11
 2#include <pybind11/pybind11.h>
 3
 4int add(int a, int b) {
 5    return a + b;
 6}
 7
 8PYBIND11_MODULE(example, m) {
 9    m.def("add", &add, "Add two integers");
10}

Header-only 設計

1pybind11 的特點:
2├── Header-only:不需要編譯函式庫
3├── C++11:使用現代 C++ 特性
4├── 自動型別轉換:Python ↔ C++ 型別
5├── 最小化模板程式碼
6└── 與 NumPy 無縫整合

型別轉換原理

1Python 呼叫 C++ 函式時:
2
3Python int  ──→  pybind11 type_caster  ──→  C++ int
4Python str  ──→  pybind11 type_caster  ──→  std::string
5Python list ──→  pybind11 type_caster  ──→  std::vector<T>
6
7回傳時反向轉換

【設計層】開發環境設定

安裝 pybind11

 1# 方法 1:pip 安裝
 2pip install pybind11
 3
 4# 方法 2:conda 安裝
 5conda install -c conda-forge pybind11
 6
 7# 方法 3:系統套件管理器
 8# Ubuntu/Debian
 9sudo apt install pybind11-dev
10
11# macOS
12brew install pybind11

專案結構

1my_project/
2├── CMakeLists.txt        # CMake 建構檔
3├── pyproject.toml        # Python 打包設定
4├── src/
5│   └── example.cpp       # C++ 原始碼
6└── tests/
7    └── test_example.py   # 測試

最小 CMakeLists.txt

1cmake_minimum_required(VERSION 3.15)
2project(example)
3
4# 找到 pybind11
5find_package(pybind11 REQUIRED)
6
7# 建立 Python 模組
8pybind11_add_module(example src/example.cpp)

使用 setup.py(簡單專案)

 1# setup.py
 2from setuptools import setup
 3from pybind11.setup_helpers import Pybind11Extension, build_ext
 4
 5ext_modules = [
 6    Pybind11Extension(
 7        "example",
 8        ["src/example.cpp"],
 9    ),
10]
11
12setup(
13    name="example",
14    ext_modules=ext_modules,
15    cmdclass={"build_ext": build_ext},
16)

【實作層】基礎綁定

函式綁定

 1#include <pybind11/pybind11.h>
 2#include <string>
 3
 4namespace py = pybind11;
 5
 6// 簡單函式
 7int add(int a, int b) {
 8    return a + b;
 9}
10
11// 帶預設參數
12double divide(double a, double b = 1.0) {
13    return a / b;
14}
15
16// 多載函式
17int multiply(int a, int b) { return a * b; }
18double multiply(double a, double b) { return a * b; }
19
20// 接受字串
21std::string greet(const std::string& name) {
22    return "Hello, " + name + "!";
23}
24
25PYBIND11_MODULE(example, m) {
26    m.doc() = "Example module";
27
28    // 基本綁定
29    m.def("add", &add, "Add two integers");
30
31    // 帶預設參數
32    m.def("divide", &divide, "Divide two numbers",
33          py::arg("a"), py::arg("b") = 1.0);
34
35    // 處理多載:需要明確指定簽名
36    m.def("multiply", py::overload_cast<int, int>(&multiply));
37    m.def("multiply", py::overload_cast<double, double>(&multiply));
38
39    // 字串函式
40    m.def("greet", &greet, "Greet someone");
41}

Python 使用:

1import example
2
3print(example.add(1, 2))           # 3
4print(example.divide(10, 2))       # 5.0
5print(example.divide(10))          # 10.0(使用預設值)
6print(example.multiply(3, 4))      # 12
7print(example.multiply(1.5, 2.0))  # 3.0
8print(example.greet("pybind11"))   # Hello, pybind11!

類別綁定

 1#include <pybind11/pybind11.h>
 2#include <string>
 3
 4namespace py = pybind11;
 5
 6class Pet {
 7public:
 8    Pet(const std::string& name, int age)
 9        : name_(name), age_(age) {}
10
11    // getter/setter
12    const std::string& get_name() const { return name_; }
13    void set_name(const std::string& name) { name_ = name; }
14
15    int get_age() const { return age_; }
16    void set_age(int age) { age_ = age; }
17
18    // 方法
19    std::string describe() const {
20        return name_ + " is " + std::to_string(age_) + " years old";
21    }
22
23private:
24    std::string name_;
25    int age_;
26};
27
28PYBIND11_MODULE(example, m) {
29    py::class_<Pet>(m, "Pet")
30        // 建構子
31        .def(py::init<const std::string&, int>())
32
33        // 屬性(getter/setter)
34        .def_property("name", &Pet::get_name, &Pet::set_name)
35        .def_property("age", &Pet::get_age, &Pet::set_age)
36
37        // 唯讀屬性
38        // .def_property_readonly("name", &Pet::get_name)
39
40        // 方法
41        .def("describe", &Pet::describe)
42
43        // __repr__
44        .def("__repr__", [](const Pet& p) {
45            return "<Pet '" + p.get_name() + "'>";
46        });
47}

繼承

 1#include <pybind11/pybind11.h>
 2#include <string>
 3
 4namespace py = pybind11;
 5
 6class Animal {
 7public:
 8    Animal(const std::string& name) : name_(name) {}
 9    virtual ~Animal() = default;
10
11    virtual std::string speak() const = 0;  // 純虛函式
12    const std::string& name() const { return name_; }
13
14protected:
15    std::string name_;
16};
17
18class Dog : public Animal {
19public:
20    Dog(const std::string& name) : Animal(name) {}
21    std::string speak() const override { return "Woof!"; }
22};
23
24class Cat : public Animal {
25public:
26    Cat(const std::string& name) : Animal(name) {}
27    std::string speak() const override { return "Meow!"; }
28};
29
30// 用於在 Python 中繼承 C++ 類別
31class PyAnimal : public Animal {
32public:
33    using Animal::Animal;
34
35    std::string speak() const override {
36        PYBIND11_OVERRIDE_PURE(std::string, Animal, speak);
37    }
38};
39
40PYBIND11_MODULE(example, m) {
41    py::class_<Animal, PyAnimal>(m, "Animal")
42        .def(py::init<const std::string&>())
43        .def("speak", &Animal::speak)
44        .def_property_readonly("name", &Animal::name);
45
46    py::class_<Dog, Animal>(m, "Dog")
47        .def(py::init<const std::string&>());
48
49    py::class_<Cat, Animal>(m, "Cat")
50        .def(py::init<const std::string&>());
51}

【實作層】進階功能

STL 容器轉換

 1#include <pybind11/pybind11.h>
 2#include <pybind11/stl.h>  // 必須包含!
 3#include <vector>
 4#include <map>
 5#include <set>
 6
 7namespace py = pybind11;
 8
 9// 自動轉換 std::vector ↔ Python list
10std::vector<int> double_values(const std::vector<int>& input) {
11    std::vector<int> result;
12    result.reserve(input.size());
13    for (int x : input) {
14        result.push_back(x * 2);
15    }
16    return result;
17}
18
19// std::map ↔ Python dict
20std::map<std::string, int> count_chars(const std::string& s) {
21    std::map<std::string, int> counts;
22    for (char c : s) {
23        counts[std::string(1, c)]++;
24    }
25    return counts;
26}
27
28PYBIND11_MODULE(example, m) {
29    m.def("double_values", &double_values);
30    m.def("count_chars", &count_chars);
31}

NumPy 整合

 1#include <pybind11/pybind11.h>
 2#include <pybind11/numpy.h>  // NumPy 支援
 3#include <cmath>
 4
 5namespace py = pybind11;
 6
 7// 處理 NumPy 陣列
 8py::array_t<double> compute_sin(py::array_t<double> input) {
 9    // 取得輸入資訊
10    auto buf = input.request();
11
12    if (buf.ndim != 1) {
13        throw std::runtime_error("輸入必須是一維陣列");
14    }
15
16    // 建立輸出陣列
17    py::array_t<double> result(buf.size);
18    auto result_buf = result.request();
19
20    // 取得原始指標
21    double* in_ptr = static_cast<double*>(buf.ptr);
22    double* out_ptr = static_cast<double*>(result_buf.ptr);
23
24    // 計算
25    for (size_t i = 0; i < buf.size; i++) {
26        out_ptr[i] = std::sin(in_ptr[i]);
27    }
28
29    return result;
30}
31
32// 多維陣列
33py::array_t<double> matrix_add(
34    py::array_t<double, py::array::c_style | py::array::forcecast> a,
35    py::array_t<double, py::array::c_style | py::array::forcecast> b
36) {
37    auto buf_a = a.request();
38    auto buf_b = b.request();
39
40    if (buf_a.ndim != 2 || buf_b.ndim != 2) {
41        throw std::runtime_error("需要二維陣列");
42    }
43
44    if (buf_a.shape[0] != buf_b.shape[0] ||
45        buf_a.shape[1] != buf_b.shape[1]) {
46        throw std::runtime_error("陣列形狀必須相同");
47    }
48
49    size_t rows = buf_a.shape[0];
50    size_t cols = buf_a.shape[1];
51
52    py::array_t<double> result({rows, cols});
53    auto buf_r = result.request();
54
55    double* ptr_a = static_cast<double*>(buf_a.ptr);
56    double* ptr_b = static_cast<double*>(buf_b.ptr);
57    double* ptr_r = static_cast<double*>(buf_r.ptr);
58
59    for (size_t i = 0; i < rows * cols; i++) {
60        ptr_r[i] = ptr_a[i] + ptr_b[i];
61    }
62
63    return result;
64}
65
66PYBIND11_MODULE(example, m) {
67    m.def("compute_sin", &compute_sin, "Compute sin for each element");
68    m.def("matrix_add", &matrix_add, "Add two matrices");
69}

GIL 管理

 1#include <pybind11/pybind11.h>
 2#include <thread>
 3#include <chrono>
 4
 5namespace py = pybind11;
 6
 7// 長時間 CPU 計算,應該釋放 GIL
 8double heavy_computation(int iterations) {
 9    // 釋放 GIL
10    py::gil_scoped_release release;
11
12    double result = 0.0;
13    for (int i = 0; i < iterations; i++) {
14        result += std::sin(i) * std::cos(i);
15    }
16
17    return result;
18    // GIL 自動重新獲取
19}
20
21// 回呼 Python 函式,需要 GIL
22void process_with_callback(py::function callback) {
23    for (int i = 0; i < 10; i++) {
24        // 如果在無 GIL 的上下文中,需要獲取
25        // py::gil_scoped_acquire acquire;
26
27        callback(i);
28    }
29}
30
31// 多執行緒範例
32std::vector<double> parallel_compute(int n_threads, int iterations) {
33    std::vector<double> results(n_threads);
34    std::vector<std::thread> threads;
35
36    {
37        // 釋放 GIL 讓執行緒可以並行
38        py::gil_scoped_release release;
39
40        for (int t = 0; t < n_threads; t++) {
41            threads.emplace_back([&results, t, iterations]() {
42                double sum = 0.0;
43                for (int i = 0; i < iterations; i++) {
44                    sum += std::sin(t + i);
45                }
46                results[t] = sum;
47            });
48        }
49
50        for (auto& thread : threads) {
51            thread.join();
52        }
53    }
54
55    return results;
56}
57
58PYBIND11_MODULE(example, m) {
59    m.def("heavy_computation", &heavy_computation);
60    m.def("process_with_callback", &process_with_callback);
61    m.def("parallel_compute", &parallel_compute);
62}

異常處理

 1#include <pybind11/pybind11.h>
 2#include <stdexcept>
 3
 4namespace py = pybind11;
 5
 6double safe_divide(double a, double b) {
 7    if (b == 0.0) {
 8        throw std::invalid_argument("除數不能為零");
 9    }
10    return a / b;
11}
12
13void custom_exception_example() {
14    // 拋出特定的 Python 異常
15    throw py::value_error("這是一個 ValueError");
16}
17
18PYBIND11_MODULE(example, m) {
19    // std::invalid_argument 自動轉換為 ValueError
20    m.def("safe_divide", &safe_divide);
21
22    m.def("custom_exception", &custom_exception_example);
23
24    // 註冊自訂異常
25    static py::exception<std::runtime_error> exc(m, "CustomError");
26    py::register_exception_translator([](std::exception_ptr p) {
27        try {
28            if (p) std::rethrow_exception(p);
29        } catch (const std::runtime_error& e) {
30            exc(e.what());
31        }
32    });
33}

【建構】現代化建構方式

scikit-build-core(推薦)

 1# pyproject.toml
 2[build-system]
 3requires = ["scikit-build-core>=0.5", "pybind11"]
 4build-backend = "scikit_build_core.build"
 5
 6[project]
 7name = "my-cpp-extension"
 8version = "0.1.0"
 9requires-python = ">=3.8"
10
11[tool.scikit-build]
12wheel.packages = ["src/my_package"]
1# CMakeLists.txt
2cmake_minimum_required(VERSION 3.15)
3project(my_cpp_extension LANGUAGES CXX)
4
5find_package(pybind11 CONFIG REQUIRED)
6
7pybind11_add_module(_core src/core.cpp)
8
9install(TARGETS _core DESTINATION .)

meson-python

1# pyproject.toml
2[build-system]
3requires = ["meson-python", "pybind11"]
4build-backend = "mesonpy"
5
6[project]
7name = "my-cpp-extension"
8version = "0.1.0"
 1# meson.build
 2project('my-cpp-extension', 'cpp',
 3  version: '0.1.0',
 4  default_options: ['cpp_std=c++17']
 5)
 6
 7pybind11 = dependency('pybind11')
 8py = import('python').find_installation(pure: false)
 9
10py.extension_module(
11  '_core',
12  'src/core.cpp',
13  dependencies: pybind11,
14  install: true
15)

【比較】pybind11 vs nanobind

nanobind 簡介

nanobind 是 pybind11 作者開發的下一代工具:

 1nanobind vs pybind11:
 2
 3nanobind:
 4├── 更小的二進位檔案(~3-5x 減少)
 5├── 更快的編譯時間
 6├── 需要 C++17
 7├── 更嚴格的型別檢查
 8└── 更好的 Free-threading 支援
 9
10pybind11:
11├── 更成熟、更多文件
12├── C++11 即可
13├── 更廣泛的社群支援
14└── 更多現有專案使用

選擇建議

 1選擇 pybind11:
 2- 需要支援舊編譯器(C++11)
 3- 需要豐富的文件和範例
 4- 現有專案已使用 pybind11
 5
 6選擇 nanobind:
 7- 新專案
 8- 追求最小二進位大小
 9- 需要更好的 Free-threading 支援
10- 可以使用 C++17

思考題

  1. pybind11 如何實現 Python 和 C++ 之間的自動型別轉換?
  2. 什麼時候應該在 C++ 程式碼中釋放 GIL?有什麼風險?
  3. 為什麼 pybind11 使用 header-only 設計?這有什麼優缺點?

實作練習

  1. 使用 pybind11 包裝一個簡單的 C++ 類別(如二維向量),支援運算子重載
  2. 實現一個接受 NumPy 陣列的 C++ 函式,計算陣列的移動平均
  3. 比較 pybind11 和 Cython 在相同任務上的效能和程式碼複雜度

延伸閱讀


上一章:Cython 下一章:選擇指南