Python 的模組化特性使得程式碼得以有效組織和重複使用。套件作為模組的集合,扮演著更重要的角色。理解套件結構的設計原則,對於構建易於維護、擴充套件和分享的程式碼函式庫至關重要。一個設計良好的套件結構,不僅能提升程式碼的可讀性和可維護性,還能簡化套件的安裝、分發和使用。在實務上,__init__.py 檔案的巧妙運用,可以控制套件名稱空間,讓使用者更便捷地使用核心功能。此外,使用 importlib.resources 有效管理套件資源,例如資料檔案或範例程式碼,能提升套件的完整性和易用性。src 佈局的採用,則能確保套件在不同環境下的一致性,並簡化測試流程。最後,瞭解套件的釋出流程,才能將開發成果順利分享給社群。
4.2 套件結構
在第3.5.2節中,我們討論瞭如何使用 poetry 這個工具來開發我們的 pycounts 套件,並且瞭解到它是如何以「可編輯」模式安裝套件的。這意味著它在你的電腦上安裝了一個指向你的套件程式碼的連結,這就是我們在上面的輸出中看到的內容。如果你使用 pip install 或 conda 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.py和module2.py是包含Python程式碼的模組。subpkg/是一個子套件,同樣包含一個__init__.py檔案。
這個結構滿足了Python套件的標準,你可以在本地電腦上從這個套件匯入內容,如果它位於目前的工作目錄中(或者它的路徑已經手動新增到 sys.path 中)。但這個套件缺乏使其可安裝所需的內容。要建立一個可安裝的套件,我們需要一個能夠安裝和構建套件的工具。目前,用於套件開發最常見的工具是 poetry、flit 和 setuptools。在本文中,我們使用 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」原則:
- 有意義:名稱應反映套件的功能。
- 易記:名稱應方便使用者找到、記住,並與其他相關套件產生聯想。
- 易於管理:由於使用者會透過點號(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 中匯入 moduleA | from package.moduleA import XXX | from .moduleA import XXX |
在 moduleC 中匯入 moduleA | from package.moduleA import XXX | from ..moduleA import XXX |
在 moduleA 中匯入 moduleC | from package.subpackage.moduleC import XXX | from .subpackage.moduleC import XXX |
雖然選擇絕對匯入還是相對匯入主要取決於個人偏好,但 PEP 8 建議使用絕對匯入,因為它們更明確。
4.2.4 __init__.py 檔案
前面提到,__init__.py 檔案用於告訴 Python 包含該檔案的目錄是一個套件。該檔案可以為空,也可以被用來向套件的名稱空間新增物件、提供檔案說明或執行其他初始化程式碼。
當匯入一個套件時,__init__.py 檔案會被執行,其中定義的任何物件都會被繫結到套件的名稱空間。例如,在 Python 套件開發中,通常會在兩個地方定義套件版本:
- 在套件的設定檔(例如
pyproject.toml)中。 - 在
__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.pycounts和pycounts.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")
內容解密:
from importlib.metadata import version:匯入version函式,用於讀取已安裝套件的版本。__version__ = version(__name__):設定__version__變數為目前套件的版本。from pycounts.pycounts import count_words和from 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"]
內容解密:
include = ["tests/*", "CHANGELOG.md"]:指定要包含在套件中的檔案或目錄。在這個例子中,我們包含了tests目錄下的所有檔案和CHANGELOG.md檔案。
在套件中包含資料
開發者經常需要在套件中包含資料檔案,例如範例資料或必要的資料檔案。有兩種常見的方法可以實作這一點:
- 將原始資料檔案包含在安裝的套件中,並提供程式碼幫助使用者載入資料。
- 在套件中包含下載資料的指令碼。
範例:在套件中包含範例文字檔
假設我們要在pycounts套件中包含一個範例文字檔flatland.txt,我們需要:
- 在
src/pycounts/目錄下建立一個新的資料子套件,並將flatland.txt放入其中。 - 建立一個新的模組
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)
內容解密:
import importlib.resources:匯入importlib.resources模組,用於存取套件中的資源。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
內容解密:
from importlib import resources:匯入importlib.resources模組,用於存取套件內的資源。def get_flatland()::定義一個函式,用於傳回flatland.txt檔案的路徑。with resources.path("pycounts.data", "flatland.txt") as f::使用resources.path方法取得flatland.txt檔案的路徑。這個方法需要兩個引數:子套件的位置 ("pycounts.data") 和檔案名稱 ("flatland.txt")。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 佈局?
強制安裝後測試:使用像 pytest 這樣的測試框架時,src 佈局強制你安裝套件後再進行測試,確保測試的是安裝後的狀態,而不是本地開發中的狀態。
乾淨的可編輯安裝:在開發過程中,使用
poetry install安裝套件時,src 佈局會將正確的路徑新增到sys.path,避免將專案根目錄下的非 Python 程式碼檔案納入可匯入範圍。通用性:src 是原始碼的通用目錄名稱,使其他開發者更容易導航你的套件。
4.3 套件釋出與安裝
本文將討論與套件釋出和安裝相關的理論知識。開發 Python 套件的典型流程如下:
- 開發套件:在本地機器上建立 Python 套件。
- 構建發行版:使用像 poetry 這樣的工具從套件構建發行版。
- 分享發行版:通常透過將發行版上傳到像 PyPI 這樣的線上倉函式庫來分享。
這些步驟構成了 Python 套件開發和分發的核心流程。瞭解這些流程對於成功釋出和管理 Python 套件至關重要。