6.5 封裝預編譯二進位
6.5 封裝預編譯二進位
本章介紹 Python 套件封裝預編譯二進位的架構模式,讓 Python 能夠調用高效能的原生程式碼。
本章目標
學完本章後,你將能夠:
- 理解「Python 封裝二進位」的架構模式
- 評估何時使用這種模式
- 了解知名套件如何應用這種技術
- 在純 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 / pybind11 | subprocess / 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. 重新打包 wheel5. 供應鏈風險
二進位來源需要信任:
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-ascii | Go | 原創實現 | 2 種 |
| beautiful-mermaid | TypeScript | 從 Go 移植並擴展 | 5 種 |
| beautiful-mermaid-py | Python | 從 TypeScript 移植 | 5 種 |
| osl-packages/mermaid-ascii | Python | 封裝 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 就能達到足夠效能架構選擇原則
- 效能驅動:只有當效能是瓶頸時才考慮封裝二進位
- 重用優先:有成熟實現時考慮封裝,否則考慮純 Python
- 維護成本:評估長期維護的複雜度
- 團隊技能:選擇團隊能夠維護的方案
延伸閱讀
- Python Packaging Guide - Binary Extensions
- manylinux 標準
- PyOxidizer 文件
- 模組四:用 C 擴展 Python - 另一種整合原生程式碼的方式
上一章:套件維護最佳實踐 下一模組:模組七:實戰效能優化