Python 包:文件结构不等于 API
这篇短文基于我之前关于 Python API 设计的一次分享中的部分内容。
在创建 Python 包时,有个很方便的地方:你只需要在包目录下添加一个 .py 源文件,用户就能立刻导入它。
比如,假设你要创建一个名为 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 究竟是从哪儿来的就不清楚了。它们是本地定义的,还是从某个模块导入的?如果是,又是哪个模块?要弄清楚,你就得滚动到文件顶部去查看导入语句,或者借助 IDE 的跳转功能。
但这个问题很容易解决:把你的包设计成通过 import lib ... lib.Thing() 的方式来使用。我们需要在包的 __init__.py 中导入 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,而你——作为库的作者——则可以把实现放在任意你想放的文件里。
随机一篇博客
评论
登录后参与讨论