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__.pyTokentokenizeをimportすればよいのです。次のようにします。

# 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)
...

Zen of Pythonにもあるように、「Flat is better than nested(平坦な方が入れ子より良い)」そして「Namespaces are one honking great idea – let’s do more of those!(名前空間はとびきり素晴らしいアイデアだ――どんどん活用しよう!)」です。

しかし、そうすることでZen of Pythonに従うだけでなく、ファイル構成をパッケージのAPIから切り離すことにもなります。ユーザーはimportしやすく読みやすい優れたAPIを手に入れることができ、ライブラリ作者であるあなたは、実装を好きなファイルに自由に配置できるのです。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント