Python 封裝與發佈(Packaging & Distribution)
主題:setup.py / setuptools
簡介
在 Python 生態系統中,**套件(package)**是程式碼重複使用與共享的核心。無論是開發內部工具,還是想把作品上傳至 PyPI 供全世界使用,都必須先把程式碼「封裝」成符合規範的套件。setup.py 搭配 setuptools 正是最常見、最成熟的封裝方式。它不僅描述套件的基本資訊(名稱、版本、作者…),更負責 依賴管理、資料檔案打包、安裝腳本生成 等工作。掌握 setup.py 的寫法,等於掌握了 Python 專案從開發階段順利走向發佈的關鍵橋樑。
本篇文章將帶你從 概念、實作、常見陷阱 以及 最佳實踐,一步步了解如何使用 setuptools 建立、測試、發佈一個完整的 Python 套件,適合剛踏入封裝領域的初學者,也能為已有經驗的開發者提供實務參考。
核心概念
1. 為什麼使用 setuptools?
setuptools是distutils的超集,支援 自動解析相依套件、入口點(entry points)、可擴充的命令 等功能。- 它已成為 Python 官方推薦的封裝工具,幾乎所有的第三方套件都以
setuptools為基礎。
2. setup.py 的基本結構
setup.py 本質上是一個普通的 Python 檔案,最核心的部份是呼叫 setuptools.setup(),傳入一系列描述套件的關鍵字參數:
# setup.py
from setuptools import setup, find_packages
setup(
name="myawesome-lib", # 套件名稱(在 PyPI 上唯一)
version="0.1.0", # 版本號,遵循 PEP 440
description="A tiny demo library", # 簡短說明
long_description=open("README.md", encoding="utf-8").read(),
long_description_content_type="text/markdown",
author="Jane Doe",
author_email="jane@example.com",
url="https://github.com/janedoe/myawesome-lib",
packages=find_packages(exclude=["tests*"]), # 自動找出套件目錄
python_requires=">=3.8", # 支援的 Python 版本
install_requires=[ # 其他套件的相依關係
"requests>=2.25",
"pandas>=1.3"
],
extras_require={ # 可選的附加相依(開發、測試等)
"dev": ["pytest>=6.0", "black"],
"docs": ["sphinx", "furo"]
},
entry_points={ # 建立可直接執行的指令
"console_scripts": [
"awesome-cli=myawesome_lib.cli:main"
]
},
include_package_data=True, # 包含 MANIFEST.in 定義的非 Python 檔案
license="MIT",
classifiers=[ # PyPI 用的分類資訊
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
)
重點:
setup.py只是一段程式碼,執行python setup.py sdist bdist_wheel時,setuptools會根據上述資訊產生分發檔(source distribution 與 wheel)。
3. find_packages() 與套件結構
find_packages() 能自動搜尋符合 Python 套件規範的目錄(必須有 __init__.py),省去手動列舉的麻煩。常見的專案結構如下:
myawesome-lib/
├── myawesome_lib/
│ ├── __init__.py
│ ├── core.py
│ └── cli.py
├── tests/
│ └── test_core.py
├── README.md
├── LICENSE
└── setup.py
只要 myawesome_lib 內有 __init__.py,find_packages() 就會把它納入 packages。
4. 建立 Wheel(.whl)
Wheel 是 Python 官方推薦的二進位分發格式,安裝速度遠快於 source distribution。只需要安裝 wheel 套件,然後執行:
$ pip install --upgrade pip setuptools wheel
$ python setup.py sdist bdist_wheel
上述指令會在 dist/ 目錄產生兩個檔案:
myawesome_lib-0.1.0.tar.gz(source distribution)myawesome_lib-0.1.0-py3-none-any.whl(wheel)
5. 發佈到 PyPI
使用 twine 進行安全上傳:
$ pip install --upgrade twine
$ twine upload dist/*
上傳前請先在 https://pypi.org/ 建立帳號,並在本機設定 ~/.pypirc(或使用環境變數 TWINE_USERNAME、TWINE_PASSWORD)以免每次手動輸入。
程式碼範例
以下提供 5 個實用範例,說明常見需求的寫法與註解。
範例 1:自訂指令(setup.py 中加入 cmdclass)
有時需要在打包前執行額外步驟(例如自動產生版本檔)。可以繼承 setuptools.Command:
# setup.py
from setuptools import setup, Command
import subprocess
import pathlib
class GenerateVersion(Command):
"""在打包前產生 _version.py 檔案"""
description = "generate version file"
user_options = [] # 不接受額外參數
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
version = "0.1.0"
target = pathlib.Path("myawesome_lib/_version.py")
target.write_text(f'__version__ = "{version}"\n')
print(f"Generated {target}")
setup(
# ... 其他參數同前
cmdclass={"gen_version": GenerateVersion},
)
執行 python setup.py gen_version 即可產生 _version.py,在正式打包前加入 python setup.py gen_version sdist bdist_wheel。
範例 2:使用 MANIFEST.in 包含非 Python 檔案
如果套件需要提供範例資料、設定檔或模型檔,必須在 MANIFEST.in 中聲明:
# MANIFEST.in
include LICENSE
include README.md
recursive-include myawesome_lib/data *.json *.csv
在 setup.py 加上 include_package_data=True 後,sdist 與 wheel 皆會把這些檔案納入。
範例 3:定義 Console Script 入口點
讓使用者安裝套件後,直接在終端機呼叫指令:
# myawesome_lib/cli.py
def main():
import argparse
parser = argparse.ArgumentParser(prog="awesome-cli")
parser.add_argument("-v", "--version", action="store_true")
args = parser.parse_args()
if args.version:
from ._version import __version__
print(f"myawesome-lib version {__version__}")
else:
print("Hello from awesome-cli!")
# 在 setup.py 中的 entry_points 已示範
安裝後,只要執行 awesome-cli --version 即可看到版本資訊。
範例 4:使用 extras_require 提供開發相依
開發者常需要額外套件(測試、文件產生),但不希望一般使用者安裝:
# setup.py 中的 extras_require 已示範
# 安裝方式
$ pip install myawesome-lib[dev] # 同時安裝 pytest、black
$ pip install myawesome-lib[docs] # 安裝 Sphinx、furo
範例 5:自動讀取長說明(Long Description)
長說明通常放在 README.md,直接讀取檔案避免硬編碼:
# setup.py
from pathlib import Path
this_dir = Path(__file__).parent
long_description = (this_dir / "README.md").read_text(encoding="utf-8")
setup(
# ...
long_description=long_description,
long_description_content_type="text/markdown",
)
若 README.md 使用 Markdown,必須在 setup() 中設定 long_description_content_type="text/markdown",讓 PyPI 正確渲染。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 建議的解決方式 |
|---|---|---|
| 版本號不符合 PEP 440 | 例如使用 v1、1_0 等非法格式。 |
使用 major.minor.patch(如 1.0.0),必要時加入 pre‑release 標記(1.0.0rc1)。 |
忘記在 MANIFEST.in 加入資料檔 |
發佈後找不到 CSV、模型檔等。 | 檢查 sdist 產出的檔案 (tar.gz) 或使用 twine check dist/*。 |
| 相依套件寫死版本 | install_requires=["numpy==1.19.2"] 會限制使用者升級。 |
使用範圍運算子(>=1.19,<2.0)或僅指定最低版本。 |
setup.py 中直接 import 套件 |
會在安裝前執行套件代碼,可能因相依未安裝而失敗。 | 只在需要時延遲 import,或將資訊寫在獨立的檔案(如 VERSION)。 |
未設定 python_requires |
允許舊版 Python 安裝,導致執行錯誤。 | 明確宣告支援的最小 Python 版本(例 >=3.8)。 |
| 忘記測試 wheel | 有些 C 擴充套件在 wheel 中會失敗。 | 使用 pip install dist/*.whl 在乾淨環境測試,或使用 cibuildwheel 產生多平台 wheel。 |
最佳實踐:
維持單一來源的版本資訊
- 建議在
myawesome_lib/_version.py中定義__version__,setup.py讀取該檔案,而不是硬編碼兩次。
- 建議在
使用
pyproject.toml作為建置前置- 從 PEP 517/518 開始,
pyproject.toml可指定建置系統,未來會逐漸取代setup.py。目前仍可保留setup.py,但加入pyproject.toml讓pip能以 PEP 517 方式建置。
- 從 PEP 517/518 開始,
自動化測試與發佈
- 使用 GitHub Actions、GitLab CI 或 Azure Pipelines,在每次 tag 時自動執行
twine upload,降低手動錯誤。
- 使用 GitHub Actions、GitLab CI 或 Azure Pipelines,在每次 tag 時自動執行
提供清晰的文件
README.md、CHANGELOG.md、CONTRIBUTING.md應放在專案根目錄,並在setup.cfg或setup.py中引用。
實際應用場景
| 場景 | 需求 | 透過 setup.py/setuptools 解決方式 |
|---|---|---|
| 內部工具套件 | 只在公司內部 pip install,無需公開 | 使用私有 PyPI(如 devpi)或直接 pip install git+https://...,setup.py 只需正確描述相依與入口點。 |
| CLI 程式 | 需要 mytool 指令直接呼叫 |
在 setup.py 中設定 entry_points["console_scripts"],安裝後自動產生執行檔。 |
| 機器學習模型套件 | 包含大型 .pt、.h5 等二進位檔 |
在 MANIFEST.in 加入 recursive-include myawesome_lib/models *.pt *.h5,並設定 include_package_data=True。 |
| 跨平台 C 擴充套件 | 必須編譯 C 程式碼 | 使用 setuptools.Extension 定義編譯參數,並透過 bdist_wheel 產生多平台 wheel;或使用 cibuildwheel 在 CI 中自動建置。 |
| 多語系文件 | 同時提供英文、中文說明 | 在 setup.cfg 或 setup.py 中使用 package_data 指定 *.md、*.rst,確保 sdist 內包含所有語言檔案。 |
總結
setup.py 搭配 setuptools 是 Python 套件封裝的核心工具。透過 setup() 的各項參數,我們可以:
- 清楚描述套件的基本資訊與相依關係
- 自動搜尋子套件、產生可執行指令、打包非程式碼資源
- 建立跨平台的 wheel,提升安裝效率
- 安全、簡潔地發佈至 PyPI,讓全世界都能使用
在實務開發中,務必遵守 PEP 440(版本規範)與 PEP 517/518(建置規範),同時配合 MANIFEST.in、extras_require、entry_points 等功能,讓套件既 易於安裝、又 易於維護。未來隨著 pyproject.toml 的普及,setup.py 仍會保留相容性,但掌握它的寫法仍是每位 Python 開發者的必備功力。
祝你在封裝與發佈的道路上 順利上傳、廣受歡迎! 🎉