Python 套件開發過程中,新增功能、管理相依性、組織程式碼和撰寫測試是確保套件品質的關鍵步驟。本文以新增繪圖功能為例,示範如何使用 matplotlib 繪製詞頻統計圖,並說明如何使用 Poetry 管理 matplotlib 等外部函式庫。同時,也介紹瞭如何組織程式碼結構,將不同功能的程式碼劃分到不同的模組中,提升程式碼的可讀性和可維護性。此外,本文還示範瞭如何使用 pytest 撰寫單元測試,確保程式碼的正確性,並使用 pytest-cov 計算測試覆寫率,評估測試的完整性。最後,本文也強調了套件檔案的重要性,並說明瞭 README、License、貢獻、行為準則、更新日誌、檔案字串、示例和 API 參考等不同型別檔案的用途和撰寫方式。

開發 Python 套件:新增相依性與程式碼組織

在上一節中,我們成功建立並安裝了一個簡單的 Python 套件。現在,我們將為這個套件新增一個新的功能:繪製詞頻統計的長條圖。

新增繪圖功能

首先,我們來看看新的函式 plot_words()。這個函式利用 Counter 物件的 .most_common() 方法,取得詞頻統計前 n 名的單字及其計數,並使用 matplotlib 套件繪製成長條圖。

import matplotlib.pyplot as plt

def plot_words(word_counts, n=10):
    """繪製詞頻統計的長條圖"""
    top_n_words = word_counts.most_common(n)
    word, count = zip(*top_n_words)
    fig = plt.bar(range(n), count)
    plt.xticks(range(n), labels=word, rotation=45)
    plt.xlabel("單字")
    plt.ylabel("計數")
    return fig

內容解密:

  1. word_counts.most_common(n):取得詞頻統計前 n 名的單字及其計數。
  2. zip(*top_n_words):將包含單字和計數的 tuple list 解壓縮成兩個獨立的 list。
  3. plt.bar(range(n), count):繪製長條圖,x 軸為單字的索引,y 軸為計數。
  4. plt.xticks(range(n), labels=word, rotation=45):設定 x 軸刻度標籤為單字,並旋轉 45 度以避免重疊。
  5. plt.xlabel("單字")plt.ylabel("計數"):設定 x 軸和 y 軸的標籤。

組織程式碼

為了保持套件程式碼的整潔和易於管理,我們將 plot_words() 函式新增到一個新的模組 plotting.py 中。這個模組將位於 src/pycounts 目錄下。

在新增 plotting.py 之後,我們的套件結構應該如下所示:

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

新增相依性

當我們嘗試匯入 plot_words() 函式時,會遇到 ModuleNotFoundError,因為 matplotlib 並不是 Python 標準函式庫的一部分。我們需要安裝它並將其新增為 pycounts 套件的相依性。

使用 poetry add 命令可以安裝指定的相依性並更新 pyproject.toml 檔案:

$ poetry add matplotlib
Using version ^3.4.3 for matplotlib
Updating dependencies
Resolving dependencies...
Writing lock file
Package operations: 8 installs, 0 updates, 0 removals
• Installing six (1.16.0)
• Installing cycler (0.10.0)
• Installing kiwisolver (1.3.1)
• Installing numpy (1.21.1)
• Installing pillow (8.3.1)
• Installing pyparsing (2.4.7)
• Installing python-dateutil (2.8.2)
• Installing matplotlib (3.4.3)

內容解密:

  1. poetry add matplotlib:安裝 matplotlib 套件並將其新增為 pycounts 的相依性。
  2. 更新後的 pyproject.toml 檔案會包含 matplotlib 的版本資訊。

版本控制

在新增新的功能和相依性之後,別忘了將變更提交到版本控制系統:

$ git add src/pycounts/plotting.py
$ git commit -m "feat: 新增繪圖功能"
$ git push

內容解密:

  1. 使用 Angular 風格的 commit message,型別為 feat,表示新增了一個新功能。
  2. 提交變更到本地和遠端版本控制系統。

透過本文的學習,我們學會瞭如何為 Python 套件新增新的功能和相依性,並且瞭解瞭如何組織程式碼和提交變更到版本控制系統。這些技能對於開發和維護一個高品質的 Python 套件至關重要。

為Python套件新增相依性

在開啟pyproject.toml檔案後,你應該可以看到matplotlib已經被列為相依性,位於[tool.poetry.dependencies]區塊下(之前只包含Python 3.9作為相依性,如我們在3.5.2節中所見):

[tool.poetry.dependencies]
python = "^3.9"
matplotlib = "^3.4.3"

現在,我們可以在Python直譯器中使用我們的套件,如下所示(請確保我們先前建立的zen.txt檔案位於目前目錄中,如果你正在執行下面的程式碼):

>>> from pycounts.pycounts import count_words
>>> from pycounts.plotting import plot_words
>>> counts = count_words("zen.txt")
>>> fig = plot_words(counts, 10)

如果在互動式IPython shell或Jupyter Notebook中執行上述Python程式碼,圖表將自動顯示。如果你正在Python直譯器中執行,則需要執行matplotlib命令plt.show()來顯示圖表,如下所示:

>>> import matplotlib.pyplot as plt
>>> plt.show()

內容解密:

  1. count_words函式用於計算文字檔案中的單詞頻率。
  2. plot_words函式用於根據單詞頻率繪製條形圖。
  3. matplotlib函式庫用於視覺化資料。

我們對套件進行了一些重要的更改,包括新增一個模組和一個相依性。使用版本控制的使用者應該提交這些更改:

$ git add src/pycounts/plotting.py
$ git commit -m "feat: add plotting module"
$ git add pyproject.toml poetry.lock
$ git commit -m "build: add matplotlib as a dependency"
$ git push

相依性版本限制

版本控制是為套件的唯一版本分配唯一識別碼的做法。例如,語意化版本控制是一種常見的版本控制系統,由三個整數A.B.C組成。A是「主要」版本,B是「次要」版本,C是「修補」版本識別碼。套件版本通常從0.1.0開始,並根據對套件所做的更改型別積極遞增主要、次要和修補程式號碼。

我們將在第7章:發布和版本控制中更詳細地討論版本控制,但現在重要的是,我們通常會限制套件相依性所需的版本號,以確保我們使用的版本是最新的並包含我們需要的功能。你可能已經注意到poetry在我們的pyproject.toml檔案中的[tool.poetry.dependencies]區塊下的相依性版本前加上了插入符號(^)運算元:

[tool.poetry.dependencies]
python = "^3.9"
matplotlib = "^3.4.3"

插入符號運算元是「需要此版本或任何更高的版本,但不修改最左邊的非零版本數字」的簡寫。例如,我們的套件相依於任何Python版本>=3.9.0和<4.0.0。因此,有效版本包括3.9.1和3.12.0,但4.0.1將無效。

內容解密:

  1. 插入符號運算元用於指定相依性版本的下限和上限。
  2. 這種方法可能會導致相依性衝突的問題。

這種方法的一個問題是,它強制任何相依於你的套件的人指定相同的限制,因此可能很難新增和解析相依性。舉個例子,流行的numpy套件的1.21.5版本對Python有繫結版本限制,需要版本>=3.7和<3.11。如果我們嘗試將這個版本的numpy新增到我們的pycounts套件中,poetry將拒絕新增它,因為我們的套件目前支援Python版本^3.9(即>=3.9.0和<4.0.0),而numpy 1.21.5只支援>=3.7和<3.11。

為瞭解決這個問題,我們有三個主要的選擇:

  1. 將我們的套件的Python版本限制更改為>=3.7和<3.11。
  2. 等待一個與我們的套件的Python限制相容的numpy版本。
  3. 手動指定可以安裝相依性的Python版本。

最終,版本限制是一個重要的問題,可能會影響你的套件的可用性。如果您打算分享您的套件,對相依性版本設定上限可能會使其他開發人員很難將您的套件用作他們自己專案中的相依性。目前,許多包裝社群,包括Python包裝管理局,通常建議除非絕對必要,否則不要對版本限制設定上限。因此,我們建議透過手動將poetry的預設插入符號運算元(^)更改為大於或等於號(>=)來指定沒有上限的版本限制。

[tool.poetry.dependencies]
python = ">=3.9"
matplotlib = ">=3.4.3"

內容解密:

  1. 版本限制對於確保套件相容性至關重要。
  2. 建議在可能的情況下避免對相依性版本設定上限。

測試你的套件

在開發完成一個能夠計算文字檔中單字數量並繪製結果的套件後,如何確保其正確運作並產生可靠的結果呢?編寫測試是一種有效的方法。本文將介紹如何為 pycounts 套件撰寫測試。

編寫測試

許多開發者已經在非正式的情況下測試他們的程式碼,透過在 Python 會話中執行幾次來檢查是否按預期工作。如果不符合預期,就修改程式碼並重複這個過程。這種方法被稱為「手動測試」或「探索性測試」。然而,在軟體開發中,更傾向於以更正式和可重複的方式定義測試。

Python 中的測試通常使用 assert 陳述式編寫。assert 會檢查一個表示式的真實性;如果表示式為真,Python 會繼續執行,但如果為假,程式碼會終止並顯示使用者定義的錯誤訊息。

使用 assert 編寫測試

讓我們為 pycounts 套件的 count_words() 函式編寫一個單元測試。單元測試評估軟體的單一「單元」,例如 Python 函式,以檢查它是否產生預期的結果。

首先,我們需要一些測試資料(稱為「fixture」),實際結果和預期結果。使用愛因斯坦的一句名言作為我們的測試資料:

“Insanity is doing the same thing over and over and expecting different results.”

手動計算名言中的單字數量(忽略大小寫和標點符號),我們得到:

einstein_counts = {'insanity': 1, 'is': 1, 'doing': 1, 'the': 1, 'same': 1, 'thing': 1, 'over': 2, 'and': 2, 'expecting': 1, 'different': 1, 'results': 1}

接下來,在 tests/ 目錄下建立一個名為 einstein.txt 的檔案,並將愛因斯坦的名言寫入其中。

quote = "Insanity is doing the same thing over and over and expecting different results."
with open("tests/einstein.txt", "w") as file:
    file.write(quote)

現在,我們可以編寫單元測試:

from pycounts.pycounts import count_words
from collections import Counter

expected = Counter({'insanity': 1, 'is': 1, 'doing': 1, 'the': 1, 'same': 1, 'thing': 1, 'over': 2, 'and': 2, 'expecting': 1, 'different': 1, 'results': 1})
actual = count_words("tests/einstein.txt")
assert actual == expected, "Einstein quote counted incorrectly!"

如果上述程式碼執行無誤,則 count_words() 函式正常運作。

自動化測試

手動編寫和執行單元測試效率低下。相反,使用「測試框架」可以自動執行測試。pytest 是 Python 套件中最常用的測試框架。

使用 pytest

  1. 將測試定義為以 test_ 開頭的函式,並包含一個或多個斷言陳述式。
  2. 將測試放在符合 test_*.py*_test.py 格式的檔案中,通常放在套件根目錄下的 tests/ 目錄中。
  3. 使用命令列中的 pytest 命令執行測試,指向測試所在的目錄。

讓我們將單元測試新增到 tests/test_pycounts.py 中:

from pycounts.pycounts import count_words
from collections import Counter

def test_count_words():
    expected = Counter({'insanity': 1, 'is': 1, 'doing': 1, 'the': 1, 'same': 1, 'thing': 1, 'over': 2, 'and': 2, 'expecting': 1, 'different': 1, 'results': 1})
    actual = count_words("tests/einstein.txt")
    assert actual == expected, "Einstein quote counted incorrectly!"

程式碼詳解:

  • test_count_words()函式:這是一個以test_開頭的函式,符合pytest的命名規範,用於定義一個測試案例。
  • expected變數:使用Counter物件儲存預期的單字計數結果。
  • actual變數:呼叫count_words("tests/einstein.txt"),計算"tests/einstein.txt"檔案中的單字數量,得到實際的計數結果。
  • assert陳述式:比較actualexpected是否相等。如果不相等,則丟擲錯誤訊息"Einstein quote counted incorrectly!",表示測試失敗。

圖表說明

此圖示展示了測試流程:

  • 載入測試資料(愛因斯坦的名言)。
  • 執行 count_words() 函式。
  • 將實際結果與預期結果進行比較。
  • 如果相符,測試透過;否則,丟擲錯誤訊息。

測試您的套件

使用pytest進行單元測試

在開發Python套件的過程中,測試是確保程式碼品質的重要步驟。我們將使用pytest來進行單元測試。首先,定義一個測試函式來驗證count_words函式的正確性:

def test_count_words():
    """測試從檔案中計數單詞。"""
    expected = Counter({'insanity': 1, 'is': 1, 'doing': 1,
                        'the': 1, 'same': 1, 'thing': 1,
                        'over': 2, 'and': 2, 'expecting': 1,
                        'different': 1, 'results': 1})
    actual = count_words("tests/einstein.txt")
    assert actual == expected, "愛因斯坦引語計數錯誤!"

內容解密:

  • test_count_words函式是一個測試案例,用於驗證count_words函式的正確性。
  • expected變數儲存了預期的單詞計數結果,使用Counter物件來表示。
  • actual變數呼叫了count_words函式,並傳入測試檔案的路徑,獲得實際的單詞計數結果。
  • assert陳述式用於比較預期結果和實際結果是否一致,如果不一致,則丟擲錯誤訊息。

在執行測試之前,需要將pytest新增為開發相依性:

$ poetry add --dev pytest

這將在pyproject.toml檔案中新增pytest至開發相依性區段:

[tool.poetry.dev-dependencies]
pytest = "^6.2.5"

接下來,可以使用以下命令執行測試:

$ pytest tests/

如果一切正常,將看到測試透過的輸出結果。

程式碼覆寫率

良好的測試套件應該盡可能地覆寫更多的程式碼。衡量測試覆寫率的一個簡單方法是計算行覆寫率,即測試執行的程式碼行數佔總行數的比例:

coverage = (lines executed / total lines) * 100%

為了計算覆寫率,可以使用pytest-cov外掛。首先,將其新增為開發相依性:

$ poetry add --dev pytest-cov

然後,執行以下命令來計算測試對pycounts套件的覆寫率:

$ pytest tests/ --cov=pycounts

輸出結果將顯示每個模組的覆寫率統計。

檔案撰寫

檔案是向使用者(包括自己)介紹套件功能和使用方法的寶貴資源。典型的Python套件包含多個部分的檔案,如下表所示:

檔案型別典型位置描述
README根目錄提供套件的整體介紹,包括功能、安裝方法和使用方法。
License根目錄說明套件的版權和授權方式。
貢獻根目錄說明如何參與專案貢獻。
行為準則根目錄定義參與專案的行為規範。
更新日誌根目錄按版本記錄套件的重要變更。
檔案字串.py檔案描述程式碼的功能和使用方法,可透過help()命令存取。
示例docs/詳細展示套件功能的逐步範例。
API參考docs/自動生成的套件功能列表及使用說明,通常使用sphinx工具建立。

檔案撰寫的典型工作流程包括三個步驟:

  1. 手動撰寫檔案。
  2. 使用sphinx工具編譯和渲染檔案為HTML格式。

Plantuml檔案結構示意圖

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Python套件開發新增相依與程式碼組織

package "資料視覺化流程" {
    package "資料準備" {
        component [資料載入] as load
        component [資料清洗] as clean
        component [資料轉換] as transform
    }

    package "圖表類型" {
        component [折線圖 Line] as line
        component [長條圖 Bar] as bar
        component [散佈圖 Scatter] as scatter
        component [熱力圖 Heatmap] as heatmap
    }

    package "美化輸出" {
        component [樣式設定] as style
        component [標籤註解] as label
        component [匯出儲存] as export
    }
}

load --> clean --> transform
transform --> line
transform --> bar
transform --> scatter
transform --> heatmap
line --> style --> export
bar --> label --> export

note right of scatter
  探索變數關係
  發現異常值
end note

@enduml

此圖示展示了Python套件中不同型別檔案的典型存放位置。

內容解密:

  • 檔案的多樣性確保了使用者可以從不同角度瞭解和使用套件。
  • 正確地使用檔案工具(如sphinx)可以簡化檔案的建立和維護工作。
  • 檔案的完整性和準確性對於套件的推廣和使用至關重要。