4.3 pybind11:現代 C++ 綁定
4.3 pybind11:現代 C++ 綁定
本章介紹 pybind11,一個輕量級的 header-only C++ 函式庫,用於建立 Python 綁定。
本章目標
學完本章後,你將能夠:
- 理解 pybind11 的設計哲學
- 建立函式和類別綁定
- 處理 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 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", ¶llel_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思考題
- pybind11 如何實現 Python 和 C++ 之間的自動型別轉換?
- 什麼時候應該在 C++ 程式碼中釋放 GIL?有什麼風險?
- 為什麼 pybind11 使用 header-only 設計?這有什麼優缺點?
實作練習
- 使用 pybind11 包裝一個簡單的 C++ 類別(如二維向量),支援運算子重載
- 實現一個接受 NumPy 陣列的 C++ 函式,計算陣列的移動平均
- 比較 pybind11 和 Cython 在相同任務上的效能和程式碼複雜度