Python 課程 – 封裝與發佈(Packaging & Distribution)
主題:wheel / dist
簡介
在 Python 生態系統中,**套件(package)**是讓程式碼得以重複使用、共享與維護的核心。
然而,僅有原始程式碼並不足以讓使用者方便地安裝與部署,特別是當套件依賴其他第三方函式庫或需要編譯 C 擴充模組時,傳統的 setup.py sdist(source distribution)往往會造成安裝時間長、環境相容性差的問題。
wheel(.whl 檔案)正是為了解決這些痛點而誕生的二進位發佈格式。它是 PEP 427 所定義的「通用二進位套件」,可以直接被 pip 安裝,省去編譯步驟、提升安裝速度,同時保留跨平台的相容性。掌握 wheel 與 dist 目錄的運作方式,對於想要 快速、可靠 發佈自己的套件至 PyPI(或內部私有庫)是必備技能。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步完成 wheel 的產生與發布,讓你的程式碼從開發階段無縫過渡到正式發佈。
核心概念
1. 什麼是 wheel?
wheel是一種 二進位發佈檔(Binary Distribution),副檔名為.whl。- 它包含已編譯好的 Python bytecode、C 擴充模組(若有)以及所有元資料(metadata),可直接被
pip install使用。 - 與傳統的
sdist(source distribution,通常是.tar.gz)不同,wheel不需要在安裝時再執行setup.py build,因此 安裝速度通常快 5~10 倍。
2. dist 目錄的角色
在使用 setuptools 或 poetry 打包時,所有產出的發佈檔案(.whl、.tar.gz)都會放在專案根目錄下的 dist/ 資料夾。
dist/是 產物(artifact) 的集中管理點,方便上傳至 PyPI、測試環境或內部 Artifactory。- 保持
dist/的乾淨(即每次重新打包前先清除舊檔)能避免意外上傳錯誤版本。
3. setup.cfg / pyproject.toml 與 setup.py 的關係
從 Python 3.8 起,官方鼓勵使用 pyproject.toml 來宣告建置系統(PEP 517/518),setup.py 只保留必要的自訂程式碼。
- 若你使用
setuptools,只需要在pyproject.toml中加入以下設定,即可使用python -m build產生 wheel:
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
4. 建立 Wheel 的流程
- 準備專案結構(
src/、tests/、README.md等)。 - 撰寫元資料(
pyproject.toml、setup.cfg)。 - 執行 建置指令:
python -m build(或python setup.py bdist_wheel)。 - 產生的
.whl會出現在dist/目錄。
程式碼範例
以下示範三個常見情境,從最簡單的純 Python 套件,到含有 C 擴充模組的進階範例,最後示範如何一次產生多平台 wheel(使用 cibuildwheel)。
範例 1:最簡單的純 Python 套件
目錄結構:
my_pkg/
├─ src/
│ └─ my_pkg/
│ └─ __init__.py
├─ pyproject.toml
└─ README.md
src/my_pkg/__init__.py
"""my_pkg - 一個簡單的工具函式庫"""
def hello(name: str) -> str:
"""回傳歡迎訊息"""
return f"Hello, {name}!"
pyproject.toml
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my-pkg"
version = "0.1.0"
description = "示範用的純 Python 套件"
authors = [{name = "Your Name", email = "you@example.com"}]
requires-python = ">=3.8"
# 若有依賴可寫在此
# dependencies = ["requests"]
建置與驗證
# 1. 安裝 build 套件(只需要一次)
pip install --upgrade build
# 2. 產生 wheel
python -m build
# 3. 查看 dist/ 內的 .whl
ls dist/
# my_pkg-0.1.0-py3-none-any.whl
提示:
-none-any表示此 wheel 為 純 Python,可在任何平台與 Python 版本上安裝。
範例 2:含 C 擴充模組的套件
假設我們要寫一個簡單的 C 函式,用於計算階乘。
目錄結構
fastfactorial/
├─ src/
│ └─ fastfactorial/
│ ├─ __init__.py
│ └─ factorial.c
├─ pyproject.toml
└─ README.md
src/fastfactorial/factorial.c
#include <Python.h>
/* 計算 n! 的遞迴實作 */
static long long fact(long long n) {
return (n <= 1) ? 1 : n * fact(n - 1);
}
/* Python 包裝函式 */
static PyObject* py_fact(PyObject* self, PyObject* args) {
long long n;
if (!PyArg_ParseTuple(args, "L", &n))
return NULL;
return PyLong_FromLongLong(fact(n));
}
/* 方法表 */
static PyMethodDef FastFactMethods[] = {
{"fact", py_fact, METH_VARARGS, "Calculate factorial of n"},
{NULL, NULL, 0, NULL}
};
/* 模組定義 */
static struct PyModuleDef fastfactmodule = {
PyModuleDef_HEAD_INIT,
"fastfactorial", /* name of module */
"Fast factorial implemented in C",
-1,
FastFactMethods
};
PyMODINIT_FUNC PyInit_fastfactorial(void) {
return PyModule_Create(&fastfactmodule);
}
src/fastfactorial/__init__.py
from .fastfactorial import fact # 直接匯入 C 擴充函式
pyproject.toml(使用 setuptools 的 Extension)
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "fastfactorial"
version = "0.2.0"
description = "使用 C 擴充加速階乘計算"
authors = [{name = "Your Name", email = "you@example.com"}]
requires-python = ">=3.8"
[tool.setuptools]
packages = ["fastfactorial"]
package-dir = {"" = "src"}
[tool.setuptools.ext-modules]
# 這裡直接使用 Python 語法描述 Extension
fastfactorial = {sources = ["src/fastfactorial/factorial.c"]}
注意:
pyproject.toml中的ext-modules仍屬於 實驗性 功能,若遇到建置失敗,可改回傳統的setup.py方式:
# setup.py (備用)
from setuptools import setup, Extension
setup(
name="fastfactorial",
version="0.2.0",
packages=["fastfactorial"],
package_dir={"": "src"},
ext_modules=[Extension("fastfactorial.fastfactorial", ["src/fastfactorial/factorial.c"])],
)
建置與測試
# 產生 wheel(會依據當前平台產生對應的 .whl)
python -m build
# 安裝到本機環境測試
pip install dist/fastfactorial-0.2.0-cp311-cp311-win_amd64.whl # 依平台不同而變
# 測試
python -c "import fastfactorial; print(fastfactorial.fact(10))"
# -> 3628800
範例 3:使用 cibuildwheel 同時產出多平台 wheel
在 CI(GitHub Actions、GitLab CI)中,我們往往希望一次建置 Linux、macOS、Windows 三大平台的 wheel,然後上傳至 PyPI。cibuildwheel 提供了簡潔的指令列工具。
pyproject.toml(保持與範例 2 相同的設定)
GitHub Actions 工作流程 (.github/workflows/wheel.yml)
name: Build & Publish Wheels
on:
push:
tags: ["v*"] # 只在打 Tag 時執行
jobs:
build_wheels:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install build tools
run: |
python -m pip install --upgrade pip
pip install build cibuildwheel==2.16.5
- name: Build wheels
run: |
python -m cibuildwheel --output-dir dist
- name: Upload wheels as artifact
uses: actions/upload-artifact@v3
with:
name: wheels-${{ matrix.os }}
path: dist/*.whl
執行結果會在每個平台產生對應的 .whl,例如:
fastfactorial-0.2.0-cp311-cp311-manylinux_2_17_x86_64.whl(Linux)fastfactorial-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl(macOS)fastfactorial-0.2.0-cp311-cp311-win_amd64.whl(Windows)
之後再使用 twine 上傳:
pip install twine
twine upload dist/*
常見陷阱與最佳實踐
| 常見問題 | 為何會發生 | 解決方式 / Best Practice |
|---|---|---|
| Wheel 只支援單一平台 | C 擴充模組在不同 OS/CPU 上需要重新編譯。 | 為每個目標平台分別建置 wheel(使用 CI + cibuildwheel),或提供 pure‑Python 版作 fallback。 |
dist/ 目錄遺留舊版檔案 |
手動刪除不徹底或忘記清理。 | 在建置前執行 rm -rf dist/ build/(Linux/macOS)或 rmdir /s /q dist build(Windows)。 |
setup.cfg 與 pyproject.toml 設定衝突 |
同時寫入兩者的 metadata,導致 pip install 時警告。 |
盡量只使用 pyproject.toml(PEP 517),或把 setup.cfg 的設定搬到 pyproject.toml。 |
缺少 wheel 套件導致建置失敗 |
只安裝了 setuptools。 |
pip install --upgrade setuptools wheel,或在 pyproject.toml 的 requires 中加入 "wheel"。 |
| 在 Windows 上編譯 C 擴充失敗 | 缺少 Visual C++ Build Tools。 | 安裝「Microsoft C++ Build Tools」或使用 conda 提供的預編譯二進位套件。 |
最佳實踐總結
- 使用
pyproject.toml宣告建置需求,保持專案結構簡潔。 - 每次發布前清空
dist/,避免誤上傳舊版。 - 針對平台依賴的套件提供 pure‑Python 後備方案,提升跨平台可用性。
- 在 CI 中自動化多平台 wheel 建置,減少手動錯誤。
- 使用
twine安全上傳,並在上傳前用twine check檢查 metadata。
實際應用場景
企業內部私有套件庫
- 使用
wheel可以在內部的 Artifactory 或 Nexus 中快速分發已編譯好的套件,減少每台機器的建置時間。
- 使用
開源函式庫發佈
- 大多數熱門的 PyPI 套件(如
numpy,pandas,requests)皆提供多平台 wheel,讓使用者在安裝時免除編譯成本。
- 大多數熱門的 PyPI 套件(如
CLI 工具的單檔發佈
- 透過
wheel搭配entry_points.console_scripts,可以一次打包成可直接執行的指令列工具,使用者只要pip install mycli即可得到可執行檔。
- 透過
嵌入式或資料科學環境
- 在 Jupyter Notebook 或 Google Colab 中安裝自訂套件時,若提供 wheel,安裝速度可從數十秒縮短至數秒,提升開發者體驗。
總結
- wheel 是 Python 發佈的事實標準,提供 二進位、即時安裝 的便利性。
dist/資料夾是所有建置產物的集中點,保持乾淨、版本一致是成功發布的關鍵。- 透過
pyproject.toml+setuptools(或poetry)即可輕鬆產生 wheel;若套件包含 C 擴充,則必須針對每個平台分別建置。 - 使用 CI(cibuildwheel) 產出多平台 wheel,搭配
twine上傳至 PyPI,實現自動化、可靠的發佈流程。
掌握上述概念與實作步驟,你就能從「寫程式」順暢過渡到「分發套件」的完整生命週期,讓自己的程式碼以最專業、最易用的方式服務給廣大的 Python 使用者。祝你打包順利、發佈成功!