本案例展示如何使用 pybind11 將 C++ 類別完整綁定到 Python,包含建構子、方法、屬性、運算子重載,以及記憶體管理與生命週期控制。

先備知識

問題背景

使用情境

在以下情境中,你可能需要在 Python 中使用 C++ 類別:

  1. 複用現有 C++ 程式庫:公司有成熟的 C++ 資料結構或演算法,想在 Python 專案中使用
  2. 效能敏感的資料處理:需要高效的記憶體管理和計算效能
  3. 自訂資料結構:標準 Python 容器無法滿足特定需求

設計目標

本案例將建立一個 StringProcessor 類別,展示:

1StringProcessor 功能:
2├── 建構子:支援預設和參數化初始化
3├── 方法:字串處理(大小寫轉換、統計、搜尋)
4├── 屬性:可讀寫的狀態屬性
5├── 運算子重載:+ 運算子串接、[] 索引存取
6├── 記憶體管理:正確處理物件生命週期
7└── 效能優化:避免不必要的記憶體複製

實作步驟

步驟 1:專案結構

 1pybind11_string_processor/
 2├── CMakeLists.txt
 3├── setup.py
 4├── src/
 5│   ├── string_processor.hpp    # C++ 標頭檔
 6│   ├── string_processor.cpp    # C++ 實作
 7│   └── bindings.cpp            # pybind11 綁定
 8├── tests/
 9│   └── test_string_processor.py
10└── benchmark.py

步驟 2:C++ 類別定義

首先,建立 C++ 類別的標頭檔:

  1// src/string_processor.hpp
  2#ifndef STRING_PROCESSOR_HPP
  3#define STRING_PROCESSOR_HPP
  4
  5#include <string>
  6#include <vector>
  7#include <unordered_map>
  8#include <memory>
  9#include <stdexcept>
 10
 11/**
 12 * StringProcessor: 高效能字串處理類別
 13 *
 14 * 提供字串操作、統計分析和搜尋功能。
 15 * 設計用於展示 pybind11 的類別綁定特性。
 16 */
 17class StringProcessor {
 18public:
 19    // ========================================
 20    // 建構子與解構子
 21    // ========================================
 22
 23    // 預設建構子
 24    StringProcessor();
 25
 26    // 參數化建構子
 27    explicit StringProcessor(const std::string& content);
 28
 29    // 複製建構子
 30    StringProcessor(const StringProcessor& other);
 31
 32    // 移動建構子
 33    StringProcessor(StringProcessor&& other) noexcept;
 34
 35    // 解構子
 36    ~StringProcessor();
 37
 38    // ========================================
 39    // 基本方法
 40    // ========================================
 41
 42    // 取得內容
 43    const std::string& content() const { return content_; }
 44
 45    // 設定內容
 46    void set_content(const std::string& content);
 47
 48    // 取得長度
 49    size_t length() const { return content_.length(); }
 50
 51    // 是否為空
 52    bool empty() const { return content_.empty(); }
 53
 54    // ========================================
 55    // 字串處理方法
 56    // ========================================
 57
 58    // 轉換為大寫
 59    std::string to_upper() const;
 60
 61    // 轉換為小寫
 62    std::string to_lower() const;
 63
 64    // 反轉字串
 65    std::string reverse() const;
 66
 67    // 移除前後空白
 68    std::string trim() const;
 69
 70    // 分割字串
 71    std::vector<std::string> split(const std::string& delimiter = " ") const;
 72
 73    // 取代子字串
 74    std::string replace(const std::string& old_str,
 75                        const std::string& new_str) const;
 76
 77    // ========================================
 78    // 統計分析方法
 79    // ========================================
 80
 81    // 字元頻率統計
 82    std::unordered_map<char, int> char_frequency() const;
 83
 84    // 單字計數
 85    size_t word_count() const;
 86
 87    // 子字串出現次數
 88    size_t count_occurrences(const std::string& substring) const;
 89
 90    // ========================================
 91    // 搜尋方法
 92    // ========================================
 93
 94    // 搜尋子字串位置(找不到回傳 -1)
 95    int find(const std::string& substring, size_t start = 0) const;
 96
 97    // 搜尋所有出現位置
 98    std::vector<size_t> find_all(const std::string& substring) const;
 99
100    // 是否包含子字串
101    bool contains(const std::string& substring) const;
102
103    // 是否以指定字串開頭
104    bool starts_with(const std::string& prefix) const;
105
106    // 是否以指定字串結尾
107    bool ends_with(const std::string& suffix) const;
108
109    // ========================================
110    // 運算子重載
111    // ========================================
112
113    // + 運算子:串接
114    StringProcessor operator+(const StringProcessor& other) const;
115
116    // += 運算子:原地串接
117    StringProcessor& operator+=(const StringProcessor& other);
118
119    // [] 運算子:索引存取
120    char operator[](size_t index) const;
121
122    // == 運算子:相等比較
123    bool operator==(const StringProcessor& other) const;
124
125    // != 運算子:不等比較
126    bool operator!=(const StringProcessor& other) const;
127
128    // ========================================
129    // 指派運算子
130    // ========================================
131
132    StringProcessor& operator=(const StringProcessor& other);
133    StringProcessor& operator=(StringProcessor&& other) noexcept;
134
135private:
136    std::string content_;
137
138    // 處理計數器(用於展示狀態追蹤)
139    mutable size_t operation_count_ = 0;
140
141    // 輔助方法
142    void increment_operation_count() const { ++operation_count_; }
143
144public:
145    // 取得操作計數(用於效能分析)
146    size_t operation_count() const { return operation_count_; }
147    void reset_operation_count() { operation_count_ = 0; }
148};
149
150#endif // STRING_PROCESSOR_HPP

步驟 3:C++ 實作

  1// src/string_processor.cpp
  2#include "string_processor.hpp"
  3#include <algorithm>
  4#include <cctype>
  5#include <sstream>
  6
  7// ========================================
  8// 建構子與解構子
  9// ========================================
 10
 11StringProcessor::StringProcessor() : content_("") {}
 12
 13StringProcessor::StringProcessor(const std::string& content)
 14    : content_(content) {}
 15
 16StringProcessor::StringProcessor(const StringProcessor& other)
 17    : content_(other.content_), operation_count_(0) {}
 18
 19StringProcessor::StringProcessor(StringProcessor&& other) noexcept
 20    : content_(std::move(other.content_)), operation_count_(0) {
 21    other.content_.clear();
 22}
 23
 24StringProcessor::~StringProcessor() = default;
 25
 26// ========================================
 27// 基本方法
 28// ========================================
 29
 30void StringProcessor::set_content(const std::string& content) {
 31    content_ = content;
 32    increment_operation_count();
 33}
 34
 35// ========================================
 36// 字串處理方法
 37// ========================================
 38
 39std::string StringProcessor::to_upper() const {
 40    increment_operation_count();
 41    std::string result = content_;
 42    std::transform(result.begin(), result.end(), result.begin(),
 43                   [](unsigned char c) { return std::toupper(c); });
 44    return result;
 45}
 46
 47std::string StringProcessor::to_lower() const {
 48    increment_operation_count();
 49    std::string result = content_;
 50    std::transform(result.begin(), result.end(), result.begin(),
 51                   [](unsigned char c) { return std::tolower(c); });
 52    return result;
 53}
 54
 55std::string StringProcessor::reverse() const {
 56    increment_operation_count();
 57    std::string result = content_;
 58    std::reverse(result.begin(), result.end());
 59    return result;
 60}
 61
 62std::string StringProcessor::trim() const {
 63    increment_operation_count();
 64    size_t start = content_.find_first_not_of(" \t\n\r\f\v");
 65    if (start == std::string::npos) {
 66        return "";
 67    }
 68    size_t end = content_.find_last_not_of(" \t\n\r\f\v");
 69    return content_.substr(start, end - start + 1);
 70}
 71
 72std::vector<std::string> StringProcessor::split(const std::string& delimiter) const {
 73    increment_operation_count();
 74    std::vector<std::string> result;
 75
 76    if (delimiter.empty()) {
 77        // 空分隔符:按字元分割
 78        for (char c : content_) {
 79            result.push_back(std::string(1, c));
 80        }
 81        return result;
 82    }
 83
 84    size_t start = 0;
 85    size_t end = content_.find(delimiter);
 86
 87    while (end != std::string::npos) {
 88        result.push_back(content_.substr(start, end - start));
 89        start = end + delimiter.length();
 90        end = content_.find(delimiter, start);
 91    }
 92
 93    result.push_back(content_.substr(start));
 94    return result;
 95}
 96
 97std::string StringProcessor::replace(const std::string& old_str,
 98                                     const std::string& new_str) const {
 99    increment_operation_count();
100    if (old_str.empty()) {
101        return content_;
102    }
103
104    std::string result = content_;
105    size_t pos = 0;
106    while ((pos = result.find(old_str, pos)) != std::string::npos) {
107        result.replace(pos, old_str.length(), new_str);
108        pos += new_str.length();
109    }
110    return result;
111}
112
113// ========================================
114// 統計分析方法
115// ========================================
116
117std::unordered_map<char, int> StringProcessor::char_frequency() const {
118    increment_operation_count();
119    std::unordered_map<char, int> freq;
120    for (char c : content_) {
121        freq[c]++;
122    }
123    return freq;
124}
125
126size_t StringProcessor::word_count() const {
127    increment_operation_count();
128    if (content_.empty()) {
129        return 0;
130    }
131
132    std::istringstream iss(content_);
133    size_t count = 0;
134    std::string word;
135    while (iss >> word) {
136        count++;
137    }
138    return count;
139}
140
141size_t StringProcessor::count_occurrences(const std::string& substring) const {
142    increment_operation_count();
143    if (substring.empty()) {
144        return 0;
145    }
146
147    size_t count = 0;
148    size_t pos = 0;
149    while ((pos = content_.find(substring, pos)) != std::string::npos) {
150        count++;
151        pos += substring.length();
152    }
153    return count;
154}
155
156// ========================================
157// 搜尋方法
158// ========================================
159
160int StringProcessor::find(const std::string& substring, size_t start) const {
161    increment_operation_count();
162    size_t pos = content_.find(substring, start);
163    return (pos == std::string::npos) ? -1 : static_cast<int>(pos);
164}
165
166std::vector<size_t> StringProcessor::find_all(const std::string& substring) const {
167    increment_operation_count();
168    std::vector<size_t> positions;
169    if (substring.empty()) {
170        return positions;
171    }
172
173    size_t pos = 0;
174    while ((pos = content_.find(substring, pos)) != std::string::npos) {
175        positions.push_back(pos);
176        pos += substring.length();
177    }
178    return positions;
179}
180
181bool StringProcessor::contains(const std::string& substring) const {
182    increment_operation_count();
183    return content_.find(substring) != std::string::npos;
184}
185
186bool StringProcessor::starts_with(const std::string& prefix) const {
187    increment_operation_count();
188    if (prefix.length() > content_.length()) {
189        return false;
190    }
191    return content_.compare(0, prefix.length(), prefix) == 0;
192}
193
194bool StringProcessor::ends_with(const std::string& suffix) const {
195    increment_operation_count();
196    if (suffix.length() > content_.length()) {
197        return false;
198    }
199    return content_.compare(content_.length() - suffix.length(),
200                            suffix.length(), suffix) == 0;
201}
202
203// ========================================
204// 運算子重載
205// ========================================
206
207StringProcessor StringProcessor::operator+(const StringProcessor& other) const {
208    increment_operation_count();
209    return StringProcessor(content_ + other.content_);
210}
211
212StringProcessor& StringProcessor::operator+=(const StringProcessor& other) {
213    increment_operation_count();
214    content_ += other.content_;
215    return *this;
216}
217
218char StringProcessor::operator[](size_t index) const {
219    increment_operation_count();
220    if (index >= content_.length()) {
221        throw std::out_of_range("Index out of range: " + std::to_string(index));
222    }
223    return content_[index];
224}
225
226bool StringProcessor::operator==(const StringProcessor& other) const {
227    return content_ == other.content_;
228}
229
230bool StringProcessor::operator!=(const StringProcessor& other) const {
231    return content_ != other.content_;
232}
233
234// ========================================
235// 指派運算子
236// ========================================
237
238StringProcessor& StringProcessor::operator=(const StringProcessor& other) {
239    if (this != &other) {
240        content_ = other.content_;
241        operation_count_ = 0;
242    }
243    return *this;
244}
245
246StringProcessor& StringProcessor::operator=(StringProcessor&& other) noexcept {
247    if (this != &other) {
248        content_ = std::move(other.content_);
249        operation_count_ = 0;
250        other.content_.clear();
251    }
252    return *this;
253}

步驟 4:pybind11 綁定

這是最關鍵的部分,將 C++ 類別暴露給 Python:

  1// src/bindings.cpp
  2#include <pybind11/pybind11.h>
  3#include <pybind11/stl.h>  // 支援 STL 容器自動轉換
  4#include <pybind11/operators.h>  // 支援運算子重載
  5
  6#include "string_processor.hpp"
  7
  8namespace py = pybind11;
  9
 10PYBIND11_MODULE(string_processor, m) {
 11    m.doc() = "StringProcessor: 高效能字串處理模組";
 12
 13    // ========================================
 14    // 類別綁定
 15    // ========================================
 16    py::class_<StringProcessor>(m, "StringProcessor")
 17        // ----------------------------------------
 18        // 建構子
 19        // ----------------------------------------
 20        .def(py::init<>(), "建立空的 StringProcessor")
 21        .def(py::init<const std::string&>(),
 22             py::arg("content"),
 23             "使用指定內容建立 StringProcessor")
 24
 25        // 複製建構(Python 的 copy 模組會使用)
 26        .def(py::init<const StringProcessor&>())
 27
 28        // ----------------------------------------
 29        // 屬性
 30        // ----------------------------------------
 31        // content 屬性:可讀寫
 32        .def_property("content",
 33                      &StringProcessor::content,
 34                      &StringProcessor::set_content,
 35                      "字串內容")
 36
 37        // 唯讀屬性
 38        .def_property_readonly("length",
 39                               &StringProcessor::length,
 40                               "字串長度")
 41        .def_property_readonly("empty",
 42                               &StringProcessor::empty,
 43                               "是否為空")
 44        .def_property_readonly("operation_count",
 45                               &StringProcessor::operation_count,
 46                               "操作計數器")
 47
 48        // ----------------------------------------
 49        // 基本方法
 50        // ----------------------------------------
 51        .def("reset_operation_count",
 52             &StringProcessor::reset_operation_count,
 53             "重置操作計數器")
 54
 55        // ----------------------------------------
 56        // 字串處理方法
 57        // ----------------------------------------
 58        .def("to_upper",
 59             &StringProcessor::to_upper,
 60             "轉換為大寫")
 61        .def("to_lower",
 62             &StringProcessor::to_lower,
 63             "轉換為小寫")
 64        .def("reverse",
 65             &StringProcessor::reverse,
 66             "反轉字串")
 67        .def("trim",
 68             &StringProcessor::trim,
 69             "移除前後空白")
 70        .def("split",
 71             &StringProcessor::split,
 72             py::arg("delimiter") = " ",
 73             "以分隔符分割字串")
 74        .def("replace",
 75             &StringProcessor::replace,
 76             py::arg("old_str"),
 77             py::arg("new_str"),
 78             "取代子字串")
 79
 80        // ----------------------------------------
 81        // 統計分析方法
 82        // ----------------------------------------
 83        .def("char_frequency",
 84             &StringProcessor::char_frequency,
 85             "統計字元頻率")
 86        .def("word_count",
 87             &StringProcessor::word_count,
 88             "計算單字數量")
 89        .def("count_occurrences",
 90             &StringProcessor::count_occurrences,
 91             py::arg("substring"),
 92             "計算子字串出現次數")
 93
 94        // ----------------------------------------
 95        // 搜尋方法
 96        // ----------------------------------------
 97        .def("find",
 98             &StringProcessor::find,
 99             py::arg("substring"),
100             py::arg("start") = 0,
101             "搜尋子字串位置(找不到回傳 -1)")
102        .def("find_all",
103             &StringProcessor::find_all,
104             py::arg("substring"),
105             "搜尋所有出現位置")
106        .def("contains",
107             &StringProcessor::contains,
108             py::arg("substring"),
109             "是否包含子字串")
110        .def("starts_with",
111             &StringProcessor::starts_with,
112             py::arg("prefix"),
113             "是否以指定字串開頭")
114        .def("ends_with",
115             &StringProcessor::ends_with,
116             py::arg("suffix"),
117             "是否以指定字串結尾")
118
119        // ----------------------------------------
120        // 運算子重載
121        // ----------------------------------------
122
123        // + 運算子:StringProcessor + StringProcessor
124        .def(py::self + py::self)
125
126        // += 運算子
127        .def(py::self += py::self)
128
129        // == 和 != 運算子
130        .def(py::self == py::self)
131        .def(py::self != py::self)
132
133        // [] 運算子:索引存取
134        .def("__getitem__",
135             &StringProcessor::operator[],
136             py::arg("index"),
137             "取得指定位置的字元")
138
139        // ----------------------------------------
140        // Python 特殊方法
141        // ----------------------------------------
142
143        // __repr__:物件表示
144        .def("__repr__",
145             [](const StringProcessor& sp) {
146                 std::string repr = "<StringProcessor content='";
147                 if (sp.length() > 50) {
148                     repr += sp.content().substr(0, 47) + "...";
149                 } else {
150                     repr += sp.content();
151                 }
152                 repr += "' length=" + std::to_string(sp.length()) + ">";
153                 return repr;
154             })
155
156        // __str__:字串轉換
157        .def("__str__",
158             [](const StringProcessor& sp) {
159                 return sp.content();
160             })
161
162        // __len__:長度
163        .def("__len__",
164             &StringProcessor::length)
165
166        // __bool__:布林轉換
167        .def("__bool__",
168             [](const StringProcessor& sp) {
169                 return !sp.empty();
170             })
171
172        // __contains__:in 運算子
173        .def("__contains__",
174             &StringProcessor::contains,
175             py::arg("substring"))
176
177        // __iter__:迭代支援
178        .def("__iter__",
179             [](const StringProcessor& sp) {
180                 return py::make_iterator(sp.content().begin(),
181                                          sp.content().end());
182             },
183             py::keep_alive<0, 1>())  // 保持物件存活
184
185        // __hash__:雜湊支援(讓物件可作為 dict key)
186        .def("__hash__",
187             [](const StringProcessor& sp) {
188                 return std::hash<std::string>{}(sp.content());
189             })
190
191        // 支援 pickle
192        .def(py::pickle(
193            // __getstate__
194            [](const StringProcessor& sp) {
195                return py::make_tuple(sp.content());
196            },
197            // __setstate__
198            [](py::tuple t) {
199                if (t.size() != 1) {
200                    throw std::runtime_error("Invalid state");
201                }
202                return StringProcessor(t[0].cast<std::string>());
203            }
204        ));
205
206    // ========================================
207    // 模組層級函式
208    // ========================================
209    m.def("concatenate",
210          [](const std::vector<StringProcessor>& processors,
211             const std::string& separator) {
212              if (processors.empty()) {
213                  return StringProcessor();
214              }
215              std::string result = processors[0].content();
216              for (size_t i = 1; i < processors.size(); i++) {
217                  result += separator + processors[i].content();
218              }
219              return StringProcessor(result);
220          },
221          py::arg("processors"),
222          py::arg("separator") = "",
223          "串接多個 StringProcessor");
224
225    // 版本資訊
226    m.attr("__version__") = "0.1.0";
227}

步驟 5:建構檔案

CMakeLists.txt

 1cmake_minimum_required(VERSION 3.15)
 2project(string_processor LANGUAGES CXX)
 3
 4set(CMAKE_CXX_STANDARD 17)
 5set(CMAKE_CXX_STANDARD_REQUIRED ON)
 6
 7# 找到 pybind11
 8find_package(pybind11 REQUIRED)
 9
10# 建立 Python 模組
11pybind11_add_module(string_processor
12    src/string_processor.cpp
13    src/bindings.cpp
14)
15
16# 包含標頭檔目錄
17target_include_directories(string_processor PRIVATE src)
18
19# 優化設定
20target_compile_options(string_processor PRIVATE
21    $<$<CXX_COMPILER_ID:GNU,Clang>:-O3 -march=native>
22    $<$<CXX_COMPILER_ID:MSVC>:/O2>
23)

setup.py

 1# setup.py
 2from setuptools import setup
 3from pybind11.setup_helpers import Pybind11Extension, build_ext
 4
 5ext_modules = [
 6    Pybind11Extension(
 7        "string_processor",
 8        sources=[
 9            "src/string_processor.cpp",
10            "src/bindings.cpp",
11        ],
12        include_dirs=["src"],
13        cxx_std=17,
14        extra_compile_args=["-O3"],
15    ),
16]
17
18setup(
19    name="string_processor",
20    version="0.1.0",
21    description="High-performance string processor using pybind11",
22    ext_modules=ext_modules,
23    cmdclass={"build_ext": build_ext},
24    python_requires=">=3.8",
25)

記憶體管理與物件生命週期

pybind11 的記憶體管理策略

pybind11 提供多種方式控制物件的所有權:

 1// 1. return_value_policy::automatic(預設)
 2// pybind11 自動決定最佳策略
 3.def("get_content", &StringProcessor::content)
 4
 5// 2. return_value_policy::copy
 6// 總是建立副本,Python 擁有副本
 7.def("get_content_copy", &StringProcessor::content,
 8     py::return_value_policy::copy)
 9
10// 3. return_value_policy::reference
11// 回傳參考,不轉移所有權(危險:可能產生懸空指標)
12.def("get_content_ref", &StringProcessor::content,
13     py::return_value_policy::reference)
14
15// 4. return_value_policy::reference_internal
16// 回傳參考,並保持父物件存活
17.def("get_content_internal", &StringProcessor::content,
18     py::return_value_policy::reference_internal)

keep_alive 策略

當物件間有依賴關係時,使用 keep_alive 確保生命週期正確:

 1// keep_alive<Nurse, Patient>
 2// Nurse: 需要被保持存活的物件的引數索引
 3// Patient: 依賴 Nurse 的物件的引數索引
 4// 0 = 回傳值, 1 = self, 2+ = 其他引數
 5
 6// 範例:迭代器需要保持原物件存活
 7.def("__iter__",
 8     [](const StringProcessor& sp) {
 9         return py::make_iterator(sp.content().begin(),
10                                  sp.content().end());
11     },
12     py::keep_alive<0, 1>())  // 回傳值(0)存活期間,self(1)必須存活

智慧指標支援

pybind11 自動支援 std::shared_ptrstd::unique_ptr

1// 使用 shared_ptr 管理物件
2py::class_<StringProcessor, std::shared_ptr<StringProcessor>>(m, "StringProcessor")
3    // ...
4
5// 工廠函式回傳 shared_ptr
6m.def("create_processor", []() {
7    return std::make_shared<StringProcessor>("factory created");
8});

Python 使用範例

基本使用

 1from string_processor import StringProcessor, concatenate
 2
 3# 建立物件
 4sp = StringProcessor("Hello, World!")
 5print(sp)           # Hello, World!
 6print(repr(sp))     # <StringProcessor content='Hello, World!' length=13>
 7print(len(sp))      # 13
 8
 9# 屬性存取
10print(sp.content)   # Hello, World!
11print(sp.length)    # 13
12print(sp.empty)     # False
13
14# 修改內容
15sp.content = "New content"
16print(sp.content)   # New content

字串處理

 1sp = StringProcessor("  Hello, Python World!  ")
 2
 3# 大小寫轉換
 4print(sp.to_upper())  # "  HELLO, PYTHON WORLD!  "
 5print(sp.to_lower())  # "  hello, python world!  "
 6
 7# 修剪空白
 8print(sp.trim())      # "Hello, Python World!"
 9
10# 反轉
11print(sp.reverse())   # "  !dlroW nohtyP ,olleH  "
12
13# 分割
14sp = StringProcessor("apple,banana,cherry")
15print(sp.split(","))  # ['apple', 'banana', 'cherry']
16
17# 取代
18print(sp.replace(",", " | "))  # "apple | banana | cherry"

統計與搜尋

 1sp = StringProcessor("hello hello world")
 2
 3# 統計
 4print(sp.word_count())              # 3
 5print(sp.count_occurrences("hello")) # 2
 6print(sp.char_frequency())          # {'h': 2, 'e': 2, 'l': 5, ...}
 7
 8# 搜尋
 9print(sp.find("world"))             # 12
10print(sp.find_all("hello"))         # [0, 6]
11print(sp.contains("world"))         # True
12print(sp.starts_with("hello"))      # True
13print(sp.ends_with("world"))        # True

運算子使用

 1sp1 = StringProcessor("Hello")
 2sp2 = StringProcessor(" World")
 3
 4# + 運算子
 5sp3 = sp1 + sp2
 6print(sp3.content)   # "Hello World"
 7
 8# += 運算子
 9sp1 += sp2
10print(sp1.content)   # "Hello World"
11
12# [] 索引
13print(sp3[0])        # 'H'
14print(sp3[6])        # 'W'
15
16# in 運算子
17print("World" in sp3)  # True
18
19# 比較運算子
20print(sp1 == sp3)    # True
21print(sp1 != sp2)    # True

Python 特殊功能

 1import copy
 2import pickle
 3
 4sp = StringProcessor("test data")
 5
 6# 迭代
 7for char in sp:
 8    print(char, end="")  # t e s t   d a t a
 9
10# 複製
11sp_copy = copy.copy(sp)
12
13# 序列化
14data = pickle.dumps(sp)
15sp_restored = pickle.loads(data)
16print(sp_restored.content)  # "test data"
17
18# 作為 dict key
19cache = {sp: "cached value"}
20print(cache[sp])  # "cached value"

效能測試

建立效能測試腳本,比較 C++ 綁定與純 Python 實作:

  1# benchmark.py
  2"""
  3效能比較:pybind11 StringProcessor vs 純 Python
  4"""
  5
  6import time
  7import statistics
  8from typing import Callable, Any
  9
 10# pybind11 版本
 11from string_processor import StringProcessor
 12
 13# 純 Python 版本
 14class PyStringProcessor:
 15    """純 Python 實作作為效能基準"""
 16
 17    def __init__(self, content: str = ""):
 18        self._content = content
 19
 20    @property
 21    def content(self) -> str:
 22        return self._content
 23
 24    @content.setter
 25    def content(self, value: str):
 26        self._content = value
 27
 28    def to_upper(self) -> str:
 29        return self._content.upper()
 30
 31    def to_lower(self) -> str:
 32        return self._content.lower()
 33
 34    def reverse(self) -> str:
 35        return self._content[::-1]
 36
 37    def split(self, delimiter: str = " ") -> list:
 38        return self._content.split(delimiter)
 39
 40    def char_frequency(self) -> dict:
 41        freq = {}
 42        for c in self._content:
 43            freq[c] = freq.get(c, 0) + 1
 44        return freq
 45
 46    def word_count(self) -> int:
 47        return len(self._content.split())
 48
 49    def count_occurrences(self, substring: str) -> int:
 50        count = 0
 51        start = 0
 52        while True:
 53            pos = self._content.find(substring, start)
 54            if pos == -1:
 55                break
 56            count += 1
 57            start = pos + len(substring)
 58        return count
 59
 60    def find_all(self, substring: str) -> list:
 61        positions = []
 62        start = 0
 63        while True:
 64            pos = self._content.find(substring, start)
 65            if pos == -1:
 66                break
 67            positions.append(pos)
 68            start = pos + len(substring)
 69        return positions
 70
 71def benchmark(func: Callable[[], Any],
 72              iterations: int = 1000,
 73              warmup: int = 100) -> dict:
 74    """執行效能測試並回傳統計資料"""
 75    # 預熱
 76    for _ in range(warmup):
 77        func()
 78
 79    # 正式測試
 80    times = []
 81    for _ in range(iterations):
 82        start = time.perf_counter()
 83        func()
 84        end = time.perf_counter()
 85        times.append((end - start) * 1000)  # 轉換為毫秒
 86
 87    return {
 88        "mean": statistics.mean(times),
 89        "stdev": statistics.stdev(times),
 90        "min": min(times),
 91        "max": max(times),
 92        "median": statistics.median(times),
 93    }
 94
 95def generate_test_content(size: int) -> str:
 96    """產生測試用字串"""
 97    base = "Hello World! This is a test string for benchmarking. "
 98    return (base * (size // len(base) + 1))[:size]
 99
100def run_benchmarks():
101    """執行所有效能測試"""
102    print("=" * 70)
103    print("StringProcessor 效能測試:pybind11 vs 純 Python")
104    print("=" * 70)
105
106    sizes = [1_000, 10_000, 100_000, 1_000_000]
107
108    for size in sizes:
109        content = generate_test_content(size)
110        cpp_sp = StringProcessor(content)
111        py_sp = PyStringProcessor(content)
112
113        print(f"\n--- 字串長度:{size:,} 字元 ---\n")
114
115        # 測試項目
116        tests = [
117            ("to_upper", lambda: cpp_sp.to_upper(), lambda: py_sp.to_upper()),
118            ("to_lower", lambda: cpp_sp.to_lower(), lambda: py_sp.to_lower()),
119            ("reverse", lambda: cpp_sp.reverse(), lambda: py_sp.reverse()),
120            ("char_frequency", lambda: cpp_sp.char_frequency(), lambda: py_sp.char_frequency()),
121            ("word_count", lambda: cpp_sp.word_count(), lambda: py_sp.word_count()),
122            ("count_occurrences", lambda: cpp_sp.count_occurrences("test"), lambda: py_sp.count_occurrences("test")),
123            ("find_all", lambda: cpp_sp.find_all("Hello"), lambda: py_sp.find_all("Hello")),
124        ]
125
126        for name, cpp_func, py_func in tests:
127            cpp_result = benchmark(cpp_func, iterations=500)
128            py_result = benchmark(py_func, iterations=500)
129
130            speedup = py_result["mean"] / cpp_result["mean"]
131
132            print(f"{name:20s}")
133            print(f"  C++:    {cpp_result['mean']:8.4f} ms (stdev: {cpp_result['stdev']:.4f})")
134            print(f"  Python: {py_result['mean']:8.4f} ms (stdev: {py_result['stdev']:.4f})")
135            print(f"  加速比: {speedup:.2f}x")
136            print()
137
138    print("=" * 70)
139
140if __name__ == "__main__":
141    run_benchmarks()

預期效能結果

 1======================================================================
 2StringProcessor 效能測試:pybind11 vs 純 Python
 3======================================================================
 4
 5--- 字串長度:1,000 字元 ---
 6
 7to_upper
 8  C++:      0.0012 ms (stdev: 0.0003)
 9  Python:   0.0008 ms (stdev: 0.0002)
10  加速比: 0.67x
11
12char_frequency
13  C++:      0.0089 ms (stdev: 0.0012)
14  Python:   0.0423 ms (stdev: 0.0045)
15  加速比: 4.75x
16
17word_count
18  C++:      0.0034 ms (stdev: 0.0008)
19  Python:   0.0028 ms (stdev: 0.0006)
20  加速比: 0.82x
21
22--- 字串長度:100,000 字元 ---
23
24to_upper
25  C++:      0.0892 ms (stdev: 0.0089)
26  Python:   0.0634 ms (stdev: 0.0067)
27  加速比: 0.71x
28
29char_frequency
30  C++:      0.7823 ms (stdev: 0.0456)
31  Python:   4.2341 ms (stdev: 0.2134)
32  加速比: 5.41x
33
34count_occurrences
35  C++:      0.0234 ms (stdev: 0.0034)
36  Python:   0.0567 ms (stdev: 0.0078)
37  加速比: 2.42x
38
39find_all
40  C++:      0.0312 ms (stdev: 0.0045)
41  Python:   0.0823 ms (stdev: 0.0098)
42  加速比: 2.64x
43
44======================================================================

效能分析

操作類型C++ 優勢說明
字元頻率統計4-6xC++ unordered_map 比 Python dict 更快
搜尋操作2-3xC++ string::find 效率高
大小寫轉換0.7xPython 內建函式已高度優化
單字計數0.8-1xPython split() 非常高效

重點觀察

  1. 不是所有操作都能加速:Python 的內建字串方法(如 upper()split())已經用 C 實作,pybind11 包裝反而增加呼叫開銷
  2. 複雜操作效益明顯:需要多次迴圈或資料結構操作的方法(如字元頻率統計)獲益最大
  3. 資料量影響顯著:資料量越大,C++ 的優勢越明顯

設計權衡

面向純 Pythonpybind11 C++ 綁定
開發速度中(需要 C++ 開發經驗)
效能基準特定操作 2-6x 加速
記憶體使用較高較低(C++ 記憶體管理)
除錯難度中高(需要 C++ 除錯工具)
部署複雜度簡單需要編譯環境
可維護性中(需要維護兩種語言)

何時使用 pybind11 綁定 C++ 類別?

適合使用

  • 已有成熟的 C++ 程式庫需要在 Python 中使用
  • 需要精細的記憶體管理
  • 效能瓶頸在資料結構操作而非 I/O
  • 需要與其他 C++ 系統整合

不建議使用

  • 純字串處理(Python 內建已很快)
  • 簡單的資料容器(用 Python dataclass 更簡潔)
  • 快速原型開發
  • 團隊沒有 C++ 經驗

練習

基礎練習

擴展 StringProcessor,新增以下方法:

  1. join(separator: str, strings: list[str]) - 用分隔符串接字串列表
  2. pad_left(width: int, char: str) - 左側填充字元
  3. pad_right(width: int, char: str) - 右側填充字元

進階練習

建立一個 DataBuffer 類別,展示:

  1. 使用 std::vector<uint8_t> 儲存二進位資料
  2. 支援 Python buffer protocol(可與 NumPy 互通)
  3. 實作切片操作(__getitem__ 支援 slice)

挑戰題

比較三種綁定方式的效能:

  1. pybind11 直接綁定
  2. pybind11 + 釋放 GIL
  3. 使用 NumPy 陣列避免資料複製

延伸閱讀


返回:案例研究 返回:模組五:用 C 擴展 Python