案例:pybind11 綁定 C++ 類別
案例:pybind11 綁定 C++ 類別
本案例展示如何使用 pybind11 將 C++ 類別完整綁定到 Python,包含建構子、方法、屬性、運算子重載,以及記憶體管理與生命週期控制。
先備知識
問題背景
使用情境
在以下情境中,你可能需要在 Python 中使用 C++ 類別:
- 複用現有 C++ 程式庫:公司有成熟的 C++ 資料結構或演算法,想在 Python 專案中使用
- 效能敏感的資料處理:需要高效的記憶體管理和計算效能
- 自訂資料結構:標準 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_ptr 和 std::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) # TruePython 特殊功能
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-6x | C++ unordered_map 比 Python dict 更快 |
| 搜尋操作 | 2-3x | C++ string::find 效率高 |
| 大小寫轉換 | 0.7x | Python 內建函式已高度優化 |
| 單字計數 | 0.8-1x | Python split() 非常高效 |
重點觀察:
- 不是所有操作都能加速:Python 的內建字串方法(如
upper()、split())已經用 C 實作,pybind11 包裝反而增加呼叫開銷 - 複雜操作效益明顯:需要多次迴圈或資料結構操作的方法(如字元頻率統計)獲益最大
- 資料量影響顯著:資料量越大,C++ 的優勢越明顯
設計權衡
| 面向 | 純 Python | pybind11 C++ 綁定 |
|---|---|---|
| 開發速度 | 快 | 中(需要 C++ 開發經驗) |
| 效能 | 基準 | 特定操作 2-6x 加速 |
| 記憶體使用 | 較高 | 較低(C++ 記憶體管理) |
| 除錯難度 | 低 | 中高(需要 C++ 除錯工具) |
| 部署複雜度 | 簡單 | 需要編譯環境 |
| 可維護性 | 高 | 中(需要維護兩種語言) |
何時使用 pybind11 綁定 C++ 類別?
適合使用:
- 已有成熟的 C++ 程式庫需要在 Python 中使用
- 需要精細的記憶體管理
- 效能瓶頸在資料結構操作而非 I/O
- 需要與其他 C++ 系統整合
不建議使用:
- 純字串處理(Python 內建已很快)
- 簡單的資料容器(用 Python dataclass 更簡潔)
- 快速原型開發
- 團隊沒有 C++ 經驗
練習
基礎練習
擴展 StringProcessor,新增以下方法:
join(separator: str, strings: list[str])- 用分隔符串接字串列表pad_left(width: int, char: str)- 左側填充字元pad_right(width: int, char: str)- 右側填充字元
進階練習
建立一個 DataBuffer 類別,展示:
- 使用
std::vector<uint8_t>儲存二進位資料 - 支援 Python buffer protocol(可與 NumPy 互通)
- 實作切片操作(
__getitem__支援 slice)
挑戰題
比較三種綁定方式的效能:
- pybind11 直接綁定
- pybind11 + 釋放 GIL
- 使用 NumPy 陣列避免資料複製
延伸閱讀
返回:案例研究 返回:模組五:用 C 擴展 Python