本章介紹 Python 套件封裝預編譯二進位的架構模式,讓 Python 能夠調用高效能的原生程式碼。

本章目標

學完本章後,你將能夠:

  1. 理解「Python 封裝二進位」的架構模式
  2. 評估何時使用這種模式
  3. 了解知名套件如何應用這種技術
  4. 在純 Python 與封裝二進位之間做出正確選擇

【概念】什麼是封裝預編譯二進位?

架構模式

這種模式將其他語言(如 Go、Rust、C++)編譯的二進位檔案,包裝在 Python 套件中:

1┌─────────────────────────────────────────┐
2│       Python API(薄封裝層)              │  ← 使用者接觸的介面
3├─────────────────────────────────────────┤
4│   subprocess / FFI / ctypes / cffi      │  ← 呼叫機制
5├─────────────────────────────────────────┤
6│   預編譯二進位(Go/Rust/C/C++)          │  ← 實際執行邏輯
7└─────────────────────────────────────────┘

與 C 擴展的差異

面向C 擴展(模組四/五)封裝預編譯二進位
編譯時機安裝時編譯發布前預編譯
使用者需求可能需要編譯器不需要編譯器
整合方式Python C API / pybind11subprocess / FFI
典型來源專為 Python 寫的擴展獨立的 CLI 工具或函式庫

【案例】

TensorFlow / PyTorch

1TensorFlow 架構:
2├── Python API(tf.* 模組)
3│   └── 使用者撰寫的程式碼
4├── 綁定層(pybind11)
5│   └── Python ↔ C++ 橋接
6└── C++ 核心 + CUDA
7    └── 預編譯的運算核心

選擇原因

  • GPU 運算需要原生效能
  • 大量的 C++ 程式碼庫
  • 安裝時編譯太慢(數小時)

cryptography

1cryptography 架構:
2├── Python API(cryptography.*)
3├── cffi 綁定層
4└── OpenSSL / BoringSSL
5    └── 預編譯的加密函式庫

選擇原因

  • 加密演算法需要經過審計的實現
  • 效能關鍵
  • 支援 PyPy(cffi 是 PyPy 官方推薦)

ruff(Python Linter)

1ruff 架構:
2├── Python 封裝(ruff 套件)
3│   └── 提供 CLI 和簡單 API
4└── Rust 二進位
5    └── 實際的 lint 邏輯

選擇原因

  • 追求極致速度(比 Flake8 快 10-100 倍)
  • Rust 的記憶體安全
  • 作為獨立工具也可使用

mermaid-ascii(PyPI 封裝)

1osl-packages/mermaid-ascii 架構:
2├── Python API(mermaid_ascii 模組)
3│   └── mermaid_to_ascii() 函式
4├── subprocess 呼叫
5└── Go 編譯的 mermaid-ascii 二進位
6    └── Mermaid → ASCII 轉換

選擇原因

  • 重用現有的 Go 實現
  • 不需要 Node.js 依賴
  • 提供 Python 友善的介面

【優點】為什麼使用這種模式?

1. 效能

核心邏輯用高效能語言實現,Python 只做介面:

1# 使用者感受不到底層是 Rust
2import ruff
3
4# 實際上是呼叫 Rust 編譯的二進位
5result = ruff.check("my_code.py")

2. 重用現有實現

不需要用 Python 重寫已經穩定的程式碼:

1情境:有一個優秀的 Go CLI 工具
2選項 A:用 Python 重寫(大量工作)
3選項 B:封裝 Go 二進位(少量工作)← 通常更好

3. 跨語言生態整合

讓 Python 使用者能夠使用其他語言的優秀工具:

1# Python 使用者不需要安裝 Go
2from mermaid_ascii import mermaid_to_ascii
3
4diagram = mermaid_to_ascii("graph LR; A-->B")

4. 維護分離

核心邏輯與 Python 封裝可以獨立更新:

1mermaid-ascii(Go): v0.6.1 → v0.7.0(核心更新)
2mermaid-ascii(PyPI): 0.6.1 → 0.7.0(同步更新封裝)

【缺點】這種模式的限制

1. 平台依賴

需要為每個作業系統和 CPU 架構提供預編譯二進位:

 1典型的 wheel 矩陣:
 2├── manylinux_x86_64
 3├── manylinux_aarch64
 4├── macosx_x86_64
 5├── macosx_arm64
 6└── win_amd64
 7
 8缺點:
 9- 維護成本高
10- 新平台支援需要時間
11- CI/CD 複雜度增加

2. 安裝體積

wheel 檔案較大(包含二進位):

1套件大小比較:
2純 Python 套件   ~50 KB
3封裝二進位套件   ~5-50 MB(依二進位大小)

3. 除錯困難

錯誤可能發生在二進位層,難以追蹤:

1# 錯誤訊息可能是二進位的 stderr
2try:
3    result = some_binary_wrapper()
4except subprocess.CalledProcessError as e:
5    # e.stderr 是二進位的錯誤訊息
6    # 可能不是 Python 友善的格式

4. 無法修改核心邏輯

想改變底層行為必須重編譯核心:

1如果你需要:
2- 修改演算法
3- 加入自訂功能
4- 修復底層 bug
5
6你必須:
71. 修改原始語言的程式碼
82. 重新編譯
93. 重新打包 wheel

5. 供應鏈風險

二進位來源需要信任:

1風險考量:
2├── 二進位是否來自可信來源?
3├── 是否有可驗證的建構流程?
4└── 是否有安全審計?
5
6最佳實踐:
7├── 使用知名維護者的套件
8├── 檢查 GitHub Actions 等 CI 建構紀錄
9└── 考慮自行建構(如果可能)

【比較】純 Python vs 封裝二進位

特性比較表

面向純 Python封裝預編譯二進位
效能較慢可達原生速度
可移植性極佳(任何有 Python 的地方)受限於預編譯平台
除錯容易(Python 工具鏈)困難(跨語言)
修改靈活度高(直接修改程式碼)低(需重新編譯)
安裝體積大(含二進位)
依賴管理簡單複雜
透明度完全可見部分黑箱
開發速度快(Python 生態)需要多語言技能

決策流程

 1選擇純 Python 如果:
 2├── 效能不是關鍵瓶頸
 3├── 需要頻繁修改邏輯
 4├── 需要最大可移植性
 5├── 功能相對簡單
 6└── 希望保持程式碼透明
 7
 8選擇封裝二進位如果:
 9├── 效能是關鍵需求
10├── 已有成熟的非 Python 實現
11├── 核心邏輯穩定,不常修改
12├── 需要系統級別的操作
13└── 安全性要求使用審計過的實現

【實作】如何封裝二進位

方法一:subprocess 呼叫

最簡單的封裝方式,適合 CLI 工具:

 1# my_wrapper/core.py
 2import subprocess
 3import shutil
 4from pathlib import Path
 5
 6def find_binary():
 7    """找到封裝的二進位檔案"""
 8    # 二進位通常放在套件目錄內
 9    package_dir = Path(__file__).parent
10    binary = package_dir / "bin" / "my_tool"
11
12    if binary.exists():
13        return binary
14
15    # 或者在 PATH 中尋找
16    return shutil.which("my_tool")
17
18def run_tool(input_text: str) -> str:
19    """呼叫封裝的工具"""
20    binary = find_binary()
21    if not binary:
22        raise RuntimeError("找不到 my_tool 二進位")
23
24    result = subprocess.run(
25        [str(binary)],
26        input=input_text,
27        capture_output=True,
28        text=True,
29        check=True
30    )
31    return result.stdout

方法二:ctypes / cffi

適合函式庫(.so / .dll):

 1# my_wrapper/bindings.py
 2import ctypes
 3from pathlib import Path
 4
 5def load_library():
 6    """載入共享函式庫"""
 7    package_dir = Path(__file__).parent
 8
 9    # 根據平台選擇正確的檔案
10    import platform
11    if platform.system() == "Darwin":
12        lib_name = "libmy_tool.dylib"
13    elif platform.system() == "Windows":
14        lib_name = "my_tool.dll"
15    else:
16        lib_name = "libmy_tool.so"
17
18    lib_path = package_dir / "lib" / lib_name
19    return ctypes.CDLL(str(lib_path))
20
21# 載入並設定函式簽名
22_lib = load_library()
23_lib.process_data.argtypes = [ctypes.c_char_p]
24_lib.process_data.restype = ctypes.c_char_p
25
26def process_data(data: str) -> str:
27    """Python 友善的介面"""
28    result = _lib.process_data(data.encode('utf-8'))
29    return result.decode('utf-8')

方法三:使用專門工具

1推薦工具:
2├── PyOxidizer:打包 Python + Rust
3├── Briefcase:跨平台打包
4├── Nuitka:Python → 原生編譯
5└── 自訂 GitHub Actions:建構多平台 wheel

【打包】建立 wheel

專案結構

 1my_package/
 2├── pyproject.toml
 3├── src/
 4│   └── my_package/
 5│       ├── __init__.py
 6│       ├── core.py          # Python 封裝
 7│       └── bin/             # 預編譯二進位
 8│           ├── my_tool-linux-x64
 9│           ├── my_tool-darwin-arm64
10│           └── my_tool-windows-x64.exe
11└── scripts/
12    └── build_binaries.sh    # 建構腳本

pyproject.toml 設定

 1[build-system]
 2requires = ["hatchling"]
 3build-backend = "hatchling.build"
 4
 5[project]
 6name = "my-package"
 7version = "0.1.0"
 8description = "Python wrapper for my_tool"
 9
10[tool.hatch.build.targets.wheel]
11# 包含二進位檔案
12include = [
13    "src/my_package/bin/*"
14]
15
16# 設定平台特定的 wheel
17[tool.hatch.build.targets.wheel.hooks.custom]
18# 自訂 hook 來處理平台特定二進位

GitHub Actions 範例

 1# .github/workflows/build.yml
 2name: Build wheels
 3
 4on: [push, release]
 5
 6jobs:
 7  build:
 8    strategy:
 9      matrix:
10        os: [ubuntu-latest, macos-latest, windows-latest]
11        arch: [x64, arm64]
12
13    runs-on: ${{ matrix.os }}
14
15    steps:
16      - uses: actions/checkout@v4
17
18      - name: Build binary
19        run: |
20          # 根據目標平台建構二進位
21          ./scripts/build_binary.sh ${{ matrix.arch }}
22
23      - name: Build wheel
24        run: |
25          pip install build
26          python -m build --wheel
27
28      - name: Upload wheel
29        uses: actions/upload-artifact@v4
30        with:
31          name: wheel-${{ matrix.os }}-${{ matrix.arch }}
32          path: dist/*.whl

【案例研究】beautiful-mermaid-py

背景

將 Mermaid 圖表轉換為 ASCII 藝術的工具,存在多種實現:

專案語言實現方式圖表支援
mermaid-asciiGo原創實現2 種
beautiful-mermaidTypeScript從 Go 移植並擴展5 種
beautiful-mermaid-pyPython從 TypeScript 移植5 種
osl-packages/mermaid-asciiPython封裝 Go 二進位2 種

兩種 Python 方案比較

1方案 A:封裝 Go 二進位(osl-packages)
2├── 優點:效能較好、維護成本低
3├── 缺點:平台依賴、無法修改邏輯
4└── 適合:追求效能、不需自訂
5
6方案 B:純 Python 移植(beautiful-mermaid-py)
7├── 優點:無依賴、可自訂、跨平台
8├── 缺點:效能略低(但對此任務足夠)
9└── 適合:需要修改、追求簡潔

決策分析

對於 Mermaid ASCII 渲染這個需求:

1效能需求:低(渲染一次圖表不需要毫秒級優化)
2修改需求:可能(未來可能想客製化輸出格式)
3平台多樣性:高(不同開發環境)
4維護成本:純 Python 更低
5
6結論:對於這個場景,純 Python 是更好的選擇

總結

何時封裝二進位

 1適合封裝二進位:
 2├── 效能關鍵的運算(加密、ML、圖像處理)
 3├── 已有成熟的非 Python 實現
 4├── 需要系統級別的操作
 5└── 安全性要求使用審計過的程式碼
 6
 7不適合封裝二進位:
 8├── 簡單的文字處理或資料轉換
 9├── 需要頻繁修改邏輯的功能
10├── 追求最大可移植性的工具
11└── 功能用純 Python 就能達到足夠效能

架構選擇原則

  1. 效能驅動:只有當效能是瓶頸時才考慮封裝二進位
  2. 重用優先:有成熟實現時考慮封裝,否則考慮純 Python
  3. 維護成本:評估長期維護的複雜度
  4. 團隊技能:選擇團隊能夠維護的方案

延伸閱讀


上一章:套件維護最佳實踐 下一模組:模組七:實戰效能優化