本文 AI 產出,尚未審核

Python 封裝與發佈(Packaging & Distribution)

主題:setup.py / setuptools


簡介

在 Python 生態系統中,**套件(package)**是程式碼重複使用與共享的核心。無論是開發內部工具,還是想把作品上傳至 PyPI 供全世界使用,都必須先把程式碼「封裝」成符合規範的套件。
setup.py 搭配 setuptools 正是最常見、最成熟的封裝方式。它不僅描述套件的基本資訊(名稱、版本、作者…),更負責 依賴管理、資料檔案打包、安裝腳本生成 等工作。掌握 setup.py 的寫法,等於掌握了 Python 專案從開發階段順利走向發佈的關鍵橋樑。

本篇文章將帶你從 概念實作常見陷阱 以及 最佳實踐,一步步了解如何使用 setuptools 建立、測試、發佈一個完整的 Python 套件,適合剛踏入封裝領域的初學者,也能為已有經驗的開發者提供實務參考。


核心概念

1. 為什麼使用 setuptools

  • setuptoolsdistutils 的超集,支援 自動解析相依套件入口點(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__.pyfind_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_USERNAMETWINE_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 後,sdistwheel 皆會把這些檔案納入。

範例 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 例如使用 v11_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。

最佳實踐

  1. 維持單一來源的版本資訊

    • 建議在 myawesome_lib/_version.py 中定義 __version__setup.py 讀取該檔案,而不是硬編碼兩次。
  2. 使用 pyproject.toml 作為建置前置

    • 從 PEP 517/518 開始,pyproject.toml 可指定建置系統,未來會逐漸取代 setup.py。目前仍可保留 setup.py,但加入 pyproject.tomlpip 能以 PEP 517 方式建置。
  3. 自動化測試與發佈

    • 使用 GitHub Actions、GitLab CI 或 Azure Pipelines,在每次 tag 時自動執行 twine upload,降低手動錯誤。
  4. 提供清晰的文件

    • README.mdCHANGELOG.mdCONTRIBUTING.md 應放在專案根目錄,並在 setup.cfgsetup.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.cfgsetup.py 中使用 package_data 指定 *.md*.rst,確保 sdist 內包含所有語言檔案。

總結

setup.py 搭配 setuptoolsPython 套件封裝的核心工具。透過 setup() 的各項參數,我們可以:

  • 清楚描述套件的基本資訊與相依關係
  • 自動搜尋子套件、產生可執行指令、打包非程式碼資源
  • 建立跨平台的 wheel,提升安裝效率
  • 安全、簡潔地發佈至 PyPI,讓全世界都能使用

在實務開發中,務必遵守 PEP 440(版本規範)與 PEP 517/518(建置規範),同時配合 MANIFEST.inextras_requireentry_points 等功能,讓套件既 易於安裝、又 易於維護。未來隨著 pyproject.toml 的普及,setup.py 仍會保留相容性,但掌握它的寫法仍是每位 Python 開發者的必備功力。

祝你在封裝與發佈的道路上 順利上傳、廣受歡迎! 🎉