本章比較各種 C 擴展工具,幫助你在不同場景下做出正確選擇。

本章目標

學完本章後,你將能夠:

  1. 根據專案需求選擇適合的工具
  2. 理解各工具的效能差異
  3. 評估維護成本與學習曲線

【總覽】C 擴展工具比較

工具定位

 1┌─────────────────────────────────────────────────────────────────┐
 2│                        C/C++ 擴展工具選擇                        │
 3├─────────────────────────────────────────────────────────────────┤
 4│                                                                 │
 5│   動態綁定(不需編譯)          靜態編譯                         │
 6│   ├── ctypes                   ├── Cython                      │
 7│   └── cffi (ABI)               ├── cffi (API)                  │
 8│                                ├── pybind11                    │
 9│                                └── nanobind                    │
10│                                                                 │
11├─────────────────────────────────────────────────────────────────┤
12│   適合 Python 背景              適合 C/C++ 背景                  │
13│   ├── ctypes                   ├── pybind11                    │
14│   ├── cffi                     ├── nanobind                    │
15│   └── Cython                   └── Python C API                │
16│                                                                 │
17└─────────────────────────────────────────────────────────────────┘

快速比較表

特性ctypescffiCythonpybind11
標準庫
不需編譯器是 (ABI)
C++ 支援有限
效能2/53/54/54/5
學習曲線中高
PyPy 支援有限有限

【決策】選擇流程圖

主要決策點

 1開始
 2 3├── Q1: 有 C/C++ 原始碼嗎?
 4│   │
 5│   ├── 沒有(只有 .so/.dll)
 6│   │   └── → ctypes 或 cffi (ABI 模式)
 7│   │
 8│   └── 有原始碼
 9│       │
10│       ├── Q2: 是 C++ 嗎?
11│       │   │
12│       │   ├── 是 C++
13│       │   │   └── → pybind11 或 nanobind
14│       │   │
15│       │   └── 是純 C
16│       │       │
17│       │       ├── Q3: 想用 Python 語法寫?
18│       │       │   ├── 是 → Cython
19│       │       │   └── 否 → cffi (API 模式)
20│       │       │
21│       │       └── Q4: 需要優化現有 Python 程式碼?
22│       │           └── 是 → Cython
2324├── Q5: 需要在 PyPy 上執行?
25│   └── 是 → cffi
2627└── Q6: 需要最小依賴?
28    └── 是 → ctypes(標準庫)

場景決策表

場景推薦工具原因
呼叫系統 API (libc, Win32)ctypes標準庫,無需額外安裝
包裝現有 C 函式庫(無原始碼)cffi (ABI)較好的型別支援
包裝現有 C 函式庫(有原始碼)cffi (API) 或 Cython效能更好
包裝 C++ 函式庫pybind11C++ 原生支援
優化 Python 程式碼瓶頸Cython漸進式優化
新專案追求最小二進位nanobind現代設計
需要跨直譯器支援 (PyPy)cffiPyPy 官方推薦

【效能】基準測試比較

測試案例:數值計算

計算 Fibonacci 數列第 n 項(迭代版):

1# 純 Python 基準
2def fib_python(n):
3    if n <= 1:
4        return n
5    a, b = 0, 1
6    for _ in range(2, n + 1):
7        a, b = b, a + b
8    return b

效能數據

 1計算 fib(40),執行 10000 次:
 2
 3工具              相對時間    說明
 4─────────────────────────────────────────
 5純 Python         1.00x      基準
 6ctypes            0.45x      呼叫 C 函式庫
 7cffi (ABI)        0.40x      動態載入
 8cffi (API)        0.15x      編譯後
 9Cython            0.08x      靜態型別
10pybind11          0.08x      C++ 編譯
11
12注意:
13- 實際數據取決於具體任務
14- 數值計算 Cython 和 pybind11 接近
15- 字串處理 pybind11 通常更快(std::string)
16- 函式呼叫開銷:ctypes > cffi ABI > cffi API ≈ Cython ≈ pybind11

函式呼叫開銷測試

 1import timeit
 2
 3# 測試空函式呼叫開銷
 4# 每個工具都實現 def noop(): pass
 5
 6results = """
 7空函式呼叫 1,000,000 次:
 8
 9工具              時間 (ms)   每次呼叫 (ns)
10───────────────────────────────────────────
11Python def        45          45
12ctypes           280         280
13cffi (ABI)       180         180
14cffi (API)        55          55
15Cython cdef       12          12  (只能從 Cython 呼叫)
16Cython cpdef      50          50
17pybind11          52          52
18"""
19
20# 結論:
21# - ctypes/cffi ABI 的呼叫開銷較大
22# - 對於頻繁呼叫的小函式,編譯方案更好
23# - Cython cdef 最快,但不能從 Python 直接呼叫

記憶體使用比較

 1匯入空模組的記憶體增加:
 2
 3工具              額外記憶體
 4────────────────────────────
 5ctypes           ~50 KB
 6cffi             ~200 KB
 7Cython module    ~100 KB
 8pybind11 module  ~150 KB
 9
10編譯後的模組大小(簡單範例):
11
12工具              .so 檔案大小
13─────────────────────────────
14Cython           ~100 KB
15pybind11         ~200 KB
16nanobind         ~50 KB

【考量】維護性與開發體驗

學習曲線

 1難度排序(由易到難):
 2
 31. ctypes
 4   - Python 標準庫
 5   - 不需要 C 知識(但需要理解 C 型別)
 6   - 文件完整
 7
 82. cffi
 9   - 需要寫 C 宣告
10   - ABI 模式很簡單
11   - API 模式需要建構設定
12
133. Cython
14   - Python 超集,逐步學習
15   - 需要理解 C 型別系統
16   - 建構系統有學習成本
17
184. pybind11
19   - 需要 C++ 知識
20   - 現代 C++ 語法
21   - 需要 CMake 或類似工具

除錯難度

 1除錯工具支援:
 2
 3ctypes:
 4├── 可以用 Python debugger
 5├── 段錯誤難以追蹤
 6└── 沒有型別檢查
 7
 8cffi:
 9├── API 模式有較好的錯誤訊息
10├── 可以用 C debugger (gdb)
11└── ABI 模式錯誤較難理解
12
13Cython:
14├── 可以產生帶行號的 C 程式碼
15├── 支援 gdb/lldb
16├── cython -a 產生效能報告
17└── 支援 Python profiler
18
19pybind11:
20├── C++ 除錯器完整支援
21├── 異常會轉換為 Python 異常
22├── 編譯錯誤訊息可能很長
23└── 模板錯誤較難理解

IDE 支援

 1IDE / 編輯器支援程度:
 2
 3工具          型別提示    自動完成    語法高亮
 4─────────────────────────────────────────────
 5ctypes        2/5         2/5         4/5
 6cffi          2/5         2/5         4/5
 7Cython        3/5         3/5         3/5
 8pybind11      4/5         4/5         4/5
 9
10註:
11- Cython 有 VSCode 和 PyCharm 插件
12- pybind11 使用標準 C++,IDE 支援完整
13- ctypes/cffi 的 Python 部分支援完整

【案例】知名專案的選擇

NumPy

 1NumPy 的策略:
 2
 3核心計算:
 4└── Python C API + 自訂機制
 5    - 歷史原因(比 pybind11 更早)
 6    - 需要極致效能
 7    - 大量使用 BLAS/LAPACK
 8
 9周邊工具:
10└── Cython
11    - 部分輔助模組
12    - 與 NumPy 陣列整合良好

SciPy

 1SciPy 的策略:
 2
 3主要使用:
 4├── Cython(大部分新程式碼)
 5├── Fortran(歷史遺留的數值庫)
 6└── C(某些核心演算法)
 7
 8選擇原因:
 9- Cython 與 NumPy 整合好
10- 漸進式優化現有 Python 程式碼
11- 科學計算社群熟悉

PyTorch

 1PyTorch 的策略:
 2
 3C++ 核心:
 4└── pybind11
 5    - 大量 C++ 程式碼
 6    - 複雜的類別層級
 7    - 自動微分需要 C++ 特性
 8
 9選擇原因:
10- C++ 是主要開發語言
11- 需要 RAII、模板、繼承
12- 與 CUDA 程式碼整合

Pillow (PIL)

 1Pillow 的策略:
 2
 3影像處理核心:
 4└── Python C API
 5    - 歷史遺留
 6    - 底層記憶體操作
 7
 8周邊功能:
 9└── 純 Python 或 ctypes
10    - 呼叫系統圖形庫

cryptography

1cryptography 的策略:
2
3主要使用:
4└── cffi
5    - 包裝 OpenSSL
6    - 需要 PyPy 支援
7    - 安全性考量(減少手動記憶體管理)

【建議】實務選擇指南

新專案建議

 12025 年新專案建議:
 2
 3場景 1: 優化 Python 程式碼
 4推薦: Cython
 5理由:
 6- 漸進式優化
 7- 熟悉的 Python 語法
 8- 與 NumPy 整合好
 9
10場景 2: 包裝現有 C++ 函式庫
11推薦: pybind11 或 nanobind
12理由:
13- C++ 原生支援
14- 現代化 API
15- 活躍的社群
16
17場景 3: 簡單呼叫系統 API
18推薦: ctypes
19理由:
20- 標準庫,無依賴
21- 簡單場景足夠
22
23場景 4: 需要 PyPy 支援
24推薦: cffi
25理由:
26- PyPy 官方推薦
27- 良好的效能

遷移建議

 1從 ctypes 遷移到更快的方案:
 2
 3如果瓶頸是呼叫頻率:
 4└── 考慮 cffi (API) 或 Cython
 5    - 減少每次呼叫的開銷
 6
 7如果瓶頸是計算本身:
 8└── 考慮 Cython
 9    - 可以優化 Python 迴圈
10    - 使用 C 型別
11
12從 Cython 遷移到 pybind11:
13
14通常不需要:
15- Cython 和 pybind11 效能相近
16- 遷移成本高
17
18考慮遷移如果:
19- 需要更多 C++ 特性
20- 團隊更熟悉 C++
21- 需要與 C++ 函式庫深度整合

混合使用

 1可以在同一專案中混合使用:
 2
 3範例結構:
 4my_package/
 5├── _core.cpython-311-xxx.so    # Cython:核心計算
 6├── _bindings.cpython-311-xxx.so # pybind11:C++ 函式庫綁定
 7├── _ffi.py                      # cffi:簡單 C 呼叫
 8└── utils.py                     # 純 Python
 9
10原則:
11- 選擇最適合該任務的工具
12- 保持介面一致(對使用者透明)
13- 文件記錄每個部分的技術選擇

【未來】發展趨勢

Free-threading 影響

 1Python 3.13+ Free-threading 的影響:
 2
 3需要注意的工具:
 4├── pybind11: 需要更新 GIL 管理方式
 5├── Cython: nogil 區塊的行為變化
 6└── cffi: 相對影響較小
 7
 8趨勢:
 9- nanobind 已有 Free-threading 支援
10- pybind11 正在積極適應
11- Cython 3.1 計劃支援

HPy:新一代 C API

 1HPy (https://hpyproject.org/)
 2
 3目標:
 4├── 統一的 C API(跨直譯器)
 5├── 更好的 PyPy/GraalPy 支援
 6├── 為 Free-threading 設計
 7└── 簡化記憶體管理
 8
 9狀態(2025):
10- 仍在開發中
11- 部分專案開始採用
12- 長期可能取代 Python C API

建構系統演進

 1建構系統趨勢:
 2
 3傳統:
 4├── setup.py + setuptools
 5└── 複雜且不標準
 6
 7現代:
 8├── scikit-build-core + CMake
 9├── meson-python
10└── 統一使用 pyproject.toml
11
12建議:
13- 新專案使用 scikit-build-core 或 meson-python
14- 舊專案可以繼續使用 setup.py
15- 避免複雜的建構邏輯

總結

選擇原則

  1. 簡單優先:如果 ctypes 能滿足需求,就用 ctypes
  2. 效能驅動:當效能成為瓶頸時,才考慮編譯方案
  3. 團隊技能:選擇團隊熟悉的技術
  4. 長期維護:考慮依賴的活躍度和未來發展

快速決策

1你應該使用:
2
3ctypes    → 簡單的系統 API 呼叫
4cffi      → 需要 PyPy 支援,或包裝 C 函式庫
5Cython    → 優化 Python 程式碼,或與 NumPy 密切整合
6pybind11  → 包裝 C++ 函式庫
7nanobind  → 新專案,追求最小化

實作練習

  1. 使用本章學到的四種工具,分別實現同一個函式(如快速排序),比較:

    • 程式碼行數
    • 編譯時間
    • 執行效能
    • 除錯體驗
  2. 選擇一個你熟悉的 C 函式庫,分別用 ctypes 和 cffi 包裝,比較開發體驗

  3. 將一個 Python 效能瓶頸用 Cython 優化,記錄優化過程和效能提升

延伸閱讀


上一章:pybind11 下一模組:模組五:用 Rust 擴展 Python