For Python packages, file structure != API

Ben Hoyt

Python 套件:檔案結構不等於 API

原文由 Ben Hoyt 發布,訂閱此部落格

這篇短文改寫自我先前一場關於 Python API 設計的演講中的部分內容

建立 Python 套件時,有個方便之處是,你只要在套件目錄中新增一個 .py 原始碼檔案,使用者就能馬上 import。

舉例來說,假設你要建立一個名為 tekst 的文字處理套件,並想加入 Token 類別和 tokenize 函式。你很自然地會把它們實作在 tekst/tokenizer.py 裡:

# tekst/tokenizer.py

@dataclass
class Token:
    kind: str
    content: str

def tokenize(text: str) -> Iterable[Token]:
    ...

到這裡都沒問題。但你不該讓使用者這樣寫:

import tekst.tokenizer

def process(token: tekst.tokenizer.Token):
    ...

tokens = tekst.tokenizer.tokenize('Hello, world')
for token in tokens:
    process(token)

除非你的文字處理函式庫真的非常龐大,否則那些 tekst.tokenizer.X 寫法既沒必要又顯得重複。當然,使用者也很可能會改成這樣寫:

from tekst.tokenizer import Token, tokenize

def process(token: Token):
    ...

tokens = tokenize('Hello, world')
for token in tokens:
    process(token)

這樣看起來就順眼多了。

不過,現在又出現了另一個問題。假設使用者的程式碼不只有短短幾行,就很難看出 Tokentokenize 是從哪裡來的。它們是在本地定義的,還是從某個模組 import 進來的?如果是後者,又是從哪個模組?這時就得捲動到檔案最上方去找 import,或是仰賴 IDE 的導覽功能。

但這個問題有個簡單的解法:把套件設計成讓使用者以 import lib ... lib.Thing() 的形式來使用。我們只需要在套件的 __init__.py 裡 import Tokentokenize,像這樣:

# tekst/__init__.py

__all__ = ['Token', 'tokenize', ...]

from .tokenizer import Token, tokenize

這樣一來,使用者的程式碼既能保持相對簡潔,又能保留函式庫名稱作為命名空間前綴,讓人一眼就看出這些東西是從哪裡來的:

import tekst

def process(token: tekst.Token):
    ...

tokens = tekst.tokenize('Hello, world')
for token in tokens:
    process(token)
...

正如 Python 之禪所說:「扁平勝於巢狀」,以及「命名空間是個絕妙的好主意——讓我們多多使用它們!」

但這麼做,你不僅遵循了 Python 之禪,還讓檔案結構與套件的 API 脫鉤。使用者得到了一個好用、易讀的漂亮 API,而你——作為函式庫的作者——則可以把實作放在任何你想放的檔案裡。

本文章由 muse-spark-1.2-contributor 進行翻譯

留言