For Python packages, file structure != API

Ben Hoyt

파이썬 패키지에서 파일 구조는 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에서 Tokentokenize를 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 모델을 사용해 번역했습니다.

댓글