返回文章列表

Python套件結構深度解析與最佳實踐

本文探討 Python 套件結構的最佳實踐,涵蓋命名規範、模組間參照、`__init__.py` 檔案的作用、src 佈局的優勢,以及如何使用 `importlib.resources` 管理套件資源和資料檔案。同時,文章也簡要介紹了 Python 套件的釋出流程,為讀者提供全面的套件開發。

Python 軟體開發

Python 的模組化特性使得程式碼得以有效組織和重複使用。套件作為模組的集合,扮演著更重要的角色。理解套件結構的設計原則,對於構建易於維護、擴充套件和分享的程式碼函式庫至關重要。一個設計良好的套件結構,不僅能提升程式碼的可讀性和可維護性,還能簡化套件的安裝、分發和使用。在實務上,__init__.py 檔案的巧妙運用,可以控制套件名稱空間,讓使用者更便捷地使用核心功能。此外,使用 importlib.resources 有效管理套件資源,例如資料檔案或範例程式碼,能提升套件的完整性和易用性。src 佈局的採用,則能確保套件在不同環境下的一致性,並簡化測試流程。最後,瞭解套件的釋出流程,才能將開發成果順利分享給社群。

4.2 套件結構

在第3.5.2節中,我們討論瞭如何使用 poetry 這個工具來開發我們的 pycounts 套件,並且瞭解到它是如何以「可編輯」模式安裝套件的。這意味著它在你的電腦上安裝了一個指向你的套件程式碼的連結,這就是我們在上面的輸出中看到的內容。如果你使用 pip installconda install 安裝 pycounts(或其他任何套件),並檢查其 __path__ 屬性,你會看到一個包含 site-packages/ 目錄的路徑,這是Python預設放置已安裝套件的地方,例如:

['/opt/miniconda/base/envs/pycounts/lib/python3.9/site-packages/pycounts']

我們將在第4.3節中進一步討論套件安裝的問題。這一切的意義在於,當你輸入 import pycounts.plotting 時,Python 首先會在由 sys.path 定義的搜尋路徑列表中搜尋名為 pycounts 的模組或套件。如果 pycounts 是一個套件,那麼它會使用 pycounts.__path__ 作為搜尋路徑(而不是 sys.path)來搜尋名為 plotting 的模組或子套件。在這一點上,我們開始涉及Python匯入系統的細微差別,並且超出了本文的範圍,但感興趣的讀者可以在Python檔案中閱讀更多關於Python匯入系統的內容。

最終,本文的重要結論是,套件是一組Python模組。它們幫助我們更好地組織和存取我們的程式碼,以及將其分發給其他人,就像我們將在第4.3節中討論的那樣。

4.2.1 套件內容

正如我們在第4.1節中討論的那樣,套件是一種組織和存取一系列模組的方法。從根本上說,一個套件被識別為一個包含 __init__.py 檔案的目錄,而一個模組是一個具有 .py 副檔名的檔案,其中包含Python程式碼。下面是一個簡單的Python套件的目錄結構範例,它包含兩個模組和一個子套件:

pkg/
├── __init__.py
├── module1.py
└── subpkg/
    ├── __init__.py
    └── module2.py

內容解密:

  • pkg/ 是套件的主目錄。
  • __init__.py 告訴Python將該目錄視為一個套件(或子套件)。
  • module1.pymodule2.py 是包含Python程式碼的模組。
  • subpkg/ 是一個子套件,同樣包含一個 __init__.py 檔案。

這個結構滿足了Python套件的標準,你可以在本地電腦上從這個套件匯入內容,如果它位於目前的工作目錄中(或者它的路徑已經手動新增到 sys.path 中)。但這個套件缺乏使其可安裝所需的內容。要建立一個可安裝的套件,我們需要一個能夠安裝和構建套件的工具。目前,用於套件開發最常見的工具是 poetryflitsetuptools。在本文中,我們使用 poetry,但我們將在第4.3.3節中比較這些工具。無論你使用哪種工具,它都依賴於一個或多個設定檔來定義你的套件的元資料和安裝指令。在一個由 poetry 管理的專案中,那個檔案是 pyproject.toml。在你的套件的根目錄中包含一個 README 檔案也是一個好的做法,用於提供關於套件的高階資訊,並將你的套件的Python程式碼放在 src/ 目錄中(我們將在第4.2.7節中討論為什麼這樣做)。因此,一個可安裝的套件的結構看起來更像是這樣:

pkg/
├── src/
│   └── pkg/
│       ├── __init__.py
│       ├── module1.py
│       └── subpkg/
│           ├── __init__.py
│           └── module2.py
├── README.md
└── pyproject.toml

內容解密:

  • 將Python程式碼放在 src/ 目錄下,有助於區分原始碼和其他支援檔案。
  • README.md 提供了關於套件的基本資訊。
  • pyproject.toml 是由 poetry 使用的設定檔,用於定義套件的元資料和依賴關係。

大多數套件除了基本的結構外,還會包含更多的內容,如詳細的檔案、測試等,就像我們在第3章中看到的那樣。由本文建立的 pycounts 套件是一個更典型的Python套件結構範例:

pycounts/
├── .readthedocs.yml
├── CHANGELOG.md
├── CONDUCT.md
├── CONTRIBUTING.md
├── docs/
│   └── ...
├── LICENSE
├── README.md
├── pyproject.toml
├── src/
│   └── pycounts/
│       ├── __init__.py
│       ├── plotting.py
│       └── pycounts.py
└── tests/
    └── ...

內容解密:

  • 檔案、測試和其他支援檔案對於開發和維護套件非常重要。
  • 當你安裝或分發你的套件給其他人時,通常只有 src/ 目錄下的Python程式碼會被包含在內。
  • 其他內容,如檔案和測試,通常透過像GitHub這樣的協作平台與其他開發者共用。

本文所描述的套件結構在技術上被稱為Python中的「常規套件」,並且是絕大多數Python套件和開發者所使用的。然而,Python還支援另一種稱為「名稱空間套件」的套件。名稱空間套件是一種將一個單一的Python套件分散到多個目錄中的方法。與所有內容都位於同一目錄層次結構中的常規套件不同,名稱空間套件可以由檔案系統上不同位置的目錄組成,並且不包含 __init__.py 檔案。

4.2 套件結構

在開發 Python 套件時,妥善規劃套件結構對於維護性和可擴充套件性至關重要。本章節將探討套件結構的關鍵要素,包括命名規範、模組間的參照、__init__.py 檔案的作用,以及如何確保套件的命名具有意義、易記且易於管理。

4.2.1 名稱空間套件簡介

名稱空間套件允許開發者將一個套件的不同部分分開開發、安裝和分發,或者將位於不同檔案系統位置的套件合併。然而,由於名稱空間套件對於初學者來說可能造成混淆,大多數開發者很少會建立名稱空間套件。因此,本文將重點放在「一般套件」上,並建議有興趣的讀者參考 PEP 420 和 Python 官方檔案。

4.2.2 套件與模組命名

選擇適當的套件和模組名稱對於建立清晰且易用的套件至關重要。Python 的命名規範在 PEP 8 和 PEP 423 中有詳細描述。主要準則包括:

  • 套件和模組應具有單一、簡短、全小寫的名稱。
  • 為了提高可讀性,可以在名稱中使用底線分隔單字,但通常不建議這麼做。

在選擇模組或套件名稱時,可以考慮以下「三個 M」原則:

  1. 有意義:名稱應反映套件的功能。
  2. 易記:名稱應方便使用者找到、記住,並與其他相關套件產生聯想。
  3. 易於管理:由於使用者會透過點號(dot notation)存取套件內容,因此應保持名稱簡短易用。例如,若將 pycounts 套件命名為 wordcountingpackage,使用者每次要存取 plotting 模組中的 plot_words() 函式時,都需要寫成 from wordcountingpackage.plotting import plot_words(),這顯然不夠簡潔。

此外,在選擇套件名稱之前,應檢查 PyPI 和其他流行的託管網站(如 GitHub、GitLab、BitBucket 等),確保所選名稱尚未被使用。

4.2.3 套件內參照

當建立包含多個模組的套件時,經常需要在一個模組中使用另一個模組的程式碼。例如,考慮以下套件結構:

src
└── package
    ├── __init__.py
    ├── moduleA.py
    ├── moduleB.py
    └── subpackage
        ├── __init__.py
        └── moduleC.py

開發者可能希望在 moduleB 中匯入 moduleA 的程式碼,這稱為「套件內參照」,可以透過「絕對匯入」或「相對匯入」來實作。

  • 絕對匯入使用套件名稱作為絕對上下文。
  • 相對匯入使用點號(dot)來表示匯入的起始位置。單個點號表示相對於目前套件(或子套件)的匯入,額外的點號可用於在套件層次結構中向上移動,每個點號代表一層。

表格 4.1 列出了根據上述套件結構的絕對匯入和相對匯入範例。

表格 4.1:絕對和相對套件內匯入範例

描述絕對匯入相對匯入
moduleB 中匯入 moduleAfrom package.moduleA import XXXfrom .moduleA import XXX
moduleC 中匯入 moduleAfrom package.moduleA import XXXfrom ..moduleA import XXX
moduleA 中匯入 moduleCfrom package.subpackage.moduleC import XXXfrom .subpackage.moduleC import XXX

雖然選擇絕對匯入還是相對匯入主要取決於個人偏好,但 PEP 8 建議使用絕對匯入,因為它們更明確。

4.2.4 __init__.py 檔案

前面提到,__init__.py 檔案用於告訴 Python 包含該檔案的目錄是一個套件。該檔案可以為空,也可以被用來向套件的名稱空間新增物件、提供檔案說明或執行其他初始化程式碼。

當匯入一個套件時,__init__.py 檔案會被執行,其中定義的任何物件都會被繫結到套件的名稱空間。例如,在 Python 套件開發中,通常會在兩個地方定義套件版本:

  1. 在套件的設定檔(例如 pyproject.toml)中。
  2. __init__.py 檔案中使用 __version__ 屬性,以便使用者快速檢查他們正在使用的套件版本。
# pycounts/__init__.py 範例
from importlib.metadata import version
__version__ = version("pycounts")

這樣,當使用者匯入 pycounts 時,可以透過 pycounts.__version__ 直接取得版本資訊。

另一個常見的使用案例是控制套件的匯入行為。例如,可以在 __init__.py 檔案中指定哪些函式或類別可以直接透過套件名稱存取,從而簡化使用者的操作。


#### 內容解密:
1. 使用 `importlib.metadata.version()` 從已安裝的套件元資料中讀取版本資訊避免在多個地方重複定義版本號
2. 將版本資訊繫結到套件的名稱空間使得使用者可以方便地查詢版本
3. 控制哪些物件被匯出到套件的名稱空間提升使用者的使用體驗

透過妥善規劃和利用這些機制,可以建立結構清晰、易於使用的 Python 套件。

套件結構與分發

自訂套件名稱空間

在開發Python套件時,如何讓使用者更方便地使用套件中的核心功能是一個重要的考量。以pycounts套件為例,假設我們有兩個核心函式:count_words()plot_words(),分別位於pycounts.pycountspycounts.plotting模組中。預設情況下,使用者需要輸入完整的路徑才能匯入這些函式:

from pycounts.pycounts import count_words
from pycounts.plotting import plot_words

為了簡化這個過程,我們可以在pycounts__init__.py檔案中匯入這些核心函式,將它們繫結到套件名稱空間。這樣,使用者就可以直接透過pycounts名稱空間存取這些函式。

__init__.py範例

# 讀取已安裝套件的版本
from importlib.metadata import version
__version__ = version(__name__)

# 將核心函式匯入套件名稱空間
from pycounts.pycounts import count_words
from pycounts.plotting import plot_words

使用範例

import pycounts
pycounts.count_words("example.txt")

內容解密:

  1. from importlib.metadata import version:匯入version函式,用於讀取已安裝套件的版本。
  2. __version__ = version(__name__):設定__version__變數為目前套件的版本。
  3. from pycounts.pycounts import count_wordsfrom pycounts.plotting import plot_words:將核心函式匯入套件名稱空間,讓使用者可以直接透過pycounts存取這些函式。

在套件中包含非程式碼檔案

當我們開發一個Python套件時,通常會包含一些非程式碼檔案,如檔案、測試資料等。這些檔案不會被包含在安裝的套件中,但我們可以透過設定pyproject.toml檔案,將特定的檔案或目錄包含在套件中。

pyproject.toml設定範例

[tool.poetry]
name = "pycounts"
version = "0.1.0"
description = "Calculate word counts in a text file!"
authors = ["Tomas Beuzen"]
license = "MIT"
readme = "README.md"
include = ["tests/*", "CHANGELOG.md"]

內容解密:

  1. include = ["tests/*", "CHANGELOG.md"]:指定要包含在套件中的檔案或目錄。在這個例子中,我們包含了tests目錄下的所有檔案和CHANGELOG.md檔案。

在套件中包含資料

開發者經常需要在套件中包含資料檔案,例如範例資料或必要的資料檔案。有兩種常見的方法可以實作這一點:

  1. 將原始資料檔案包含在安裝的套件中,並提供程式碼幫助使用者載入資料。
  2. 在套件中包含下載資料的指令碼。

範例:在套件中包含範例文字檔

假設我們要在pycounts套件中包含一個範例文字檔flatland.txt,我們需要:

  1. src/pycounts/目錄下建立一個新的資料子套件,並將flatland.txt放入其中。
  2. 建立一個新的模組datasets.py,並在其中提供程式碼幫助使用者載入資料。
目錄結構範例
pycounts/
├── src/
│   └── pycounts/
│       ├── __init__.py
│       ├── data/
│       │   ├── __init__.py
│       │   └── flatland.txt
│       ├── datasets.py
│       ├── plotting.py
│       └── pycounts.py
└── ...其他檔案...
datasets.py範例
import importlib.resources

def get_flatland_path():
    with importlib.resources.path('pycounts.data', 'flatland.txt') as f:
        return f

# 使用範例
flatland_path = get_flatland_path()
print(flatland_path)

內容解密:

  1. import importlib.resources:匯入importlib.resources模組,用於存取套件中的資源。
  2. get_flatland_path()函式:使用importlib.resources.path()函式取得flatland.txt的路徑,並傳回給使用者。

4.2 套件結構進階探討

在前面的章節中,我們已經介紹瞭如何使用 importlib.resources 模組來存取套件內的資料檔案。下面是一個具體的例子,展示瞭如何在 pycounts 套件中使用這個功能。

使用 importlib.resources 存取資料檔案

首先,我們需要在 datasets.py 檔案中定義一個函式,用於傳回資料檔案的路徑。以下是一個範例實作:

from importlib import resources

def get_flatland():
    """取得範例「Flatland」[1]_ 文字檔案的路徑。

    Returns
    
---
-
---
    pathlib.PosixPath
        檔案路徑。

    References
    
---
-
---
---
    .. [1] E. A. Abbott, ”Flatland”, Seeley & Co., 1884.
    """
    with resources.path("pycounts.data", "flatland.txt") as f:
        data_file_path = f
    return data_file_path

內容解密:

  1. from importlib import resources:匯入 importlib.resources 模組,用於存取套件內的資源。
  2. def get_flatland()::定義一個函式,用於傳回 flatland.txt 檔案的路徑。
  3. with resources.path("pycounts.data", "flatland.txt") as f::使用 resources.path 方法取得 flatland.txt 檔案的路徑。這個方法需要兩個引數:子套件的位置 ("pycounts.data") 和檔案名稱 ("flatland.txt")。
  4. return data_file_path:傳回取得的檔案路徑。

使用這個函式,可以輕易地存取 pycounts 套件內的 flatland.txt 檔案。例如:

>>> from pycounts.datasets import get_flatland
>>> get_flatland()
PosixPath('/Users/tomasbeuzen/pycounts/src/pycounts/data/flatland.txt')

接著,可以將這個路徑傳遞給 count_words() 函式,以計算文字檔案中的詞頻:

>>> from pycounts.pycounts import count_words
>>> from pycounts.datasets import get_flatland
>>> flatland_path = get_flatland()
>>> count_words(flatland_path)
Counter({'the': 2244, 'of': 1597, 'to': 1078, 'and': 1074, 'a': 902, 'i': 706, 'in': 698, 'that': 486, ...})

套件結構:src 佈局 vs. 非 src 佈局

在開發 Python 套件時,常見的兩種目錄結構是「src」佈局和「非 src」佈局。下面是這兩種佈局的例子:

src 佈局:

pkg
├── ...
├── src
│   └── pkg
│       ├── __init__.py
│       ├── module1.py
│       └── subpkg
│           ├── __init__.py
│           └── module2.py
└── ...

非 src 佈局:

pkg
├── ...
├── pkg
│   ├── __init__.py
│   ├── module1.py
│   └── subpkg
│       ├── __init__.py
│       └── module2.py
└── ...

為什麼推薦使用 src 佈局?

  1. 強制安裝後測試:使用像 pytest 這樣的測試框架時,src 佈局強制你安裝套件後再進行測試,確保測試的是安裝後的狀態,而不是本地開發中的狀態。

  2. 乾淨的可編輯安裝:在開發過程中,使用 poetry install 安裝套件時,src 佈局會將正確的路徑新增到 sys.path,避免將專案根目錄下的非 Python 程式碼檔案納入可匯入範圍。

  3. 通用性:src 是原始碼的通用目錄名稱,使其他開發者更容易導航你的套件。

4.3 套件釋出與安裝

本文將討論與套件釋出和安裝相關的理論知識。開發 Python 套件的典型流程如下:

  1. 開發套件:在本地機器上建立 Python 套件。
  2. 構建發行版:使用像 poetry 這樣的工具從套件構建發行版。
  3. 分享發行版:通常透過將發行版上傳到像 PyPI 這樣的線上倉函式庫來分享。

這些步驟構成了 Python 套件開發和分發的核心流程。瞭解這些流程對於成功釋出和管理 Python 套件至關重要。