Python 套件:檔案結構不等於 API
這篇短文改寫自我先前一場關於 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)這樣看起來就順眼多了。
不過,現在又出現了另一個問題。假設使用者的程式碼不只有短短幾行,就很難看出 Token 和 tokenize 是從哪裡來的。它們是在本地定義的,還是從某個模組 import 進來的?如果是後者,又是從哪個模組?這時就得捲動到檔案最上方去找 import,或是仰賴 IDE 的導覽功能。
但這個問題有個簡單的解法:把套件設計成讓使用者以 import lib ... lib.Thing() 的形式來使用。我們只需要在套件的 __init__.py 裡 import Token 和 tokenize,像這樣:
# 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,而你——作為函式庫的作者——則可以把實作放在任何你想放的檔案裡。
隨機一篇部落格
留言
登入後參與討論