프로그램이 커지면 코드를 여러 파일로 나누고, 남이 만든 라이브러리를 가져다 쓰고, 때로는 내 코드를 다른 사람에게 배포하게 됩니다. 이 장에서는 모듈과 패키지의 구조, import 문의 여러 형태, pip와 requirements 파일로 의존성을 관리하는 법, 그리고 pyproject.toml로 프로젝트를 정의하고 배포하는 기초를 다룹니다.
모듈과 패키지
Python에서 .py 파일 하나가 곧 모듈입니다. 파일 이름(확장자 제외)이 모듈 이름이 됩니다. 여러 모듈을 폴더로 묶은 것이 패키지이며, 폴더 안에 __init__.py 파일을 두는 것이 일반적입니다. __init__.py는 비어 있어도 되고, 패키지를 가져올 때 실행할 초기화 코드나 바깥에 공개할 이름을 둘 수도 있습니다.
myapp/
pyproject.toml
src/
myapp/
__init__.py
cli.py
text/
__init__.py
slug.py
tests/
test_slug.py
위처럼 패키지를 src/ 아래에 두는 방식을 src 레이아웃이라고 합니다. 설치하지 않은 코드가 실수로 import되는 일을 막아 주므로 라이브러리 프로젝트에서 널리 씁니다.
import 하기
import에는 몇 가지 형태가 있습니다. 모듈은 처음 import될 때 한 번만 실행되고, 그 뒤로는 캐시된 객체를 다시 씁니다.
import json # 모듈 전체
import datetime as dt # 별칭
from pathlib import Path # 특정 이름만
from collections import Counter, defaultdict
data = json.loads('{"a": 1}')
today = dt.date.today()
config = Path("config") / "app.toml"
패키지 안에서는 같은 패키지의 다른 모듈을 상대 경로로 가져올 수 있습니다. 점 하나(.)는 현재 패키지, 점 두 개(..)는 상위 패키지를 뜻합니다.
# src/myapp/text/slug.py
import re
def slugify(title: str) -> str:
return re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")
# src/myapp/cli.py
from .text.slug import slugify
def main() -> None:
print(slugify("Hello Python World")) # hello-python-world
from module import *는 어떤 이름이 들어오는지 알기 어려우므로 피합니다. 또 두 모듈이 서로를 import하는 순환 참조가 생기면 오류가 나기 쉬우니, 공통 코드를 별도 모듈로 빼서 해결합니다.
pip와 requirements 파일
애플리케이션을 다른 컴퓨터나 서버에서 똑같이 재현하려면 의존성 목록이 필요합니다. 가장 단순한 방법은 requirements.txt입니다.
# 현재 환경의 패키지와 정확한 버전을 기록합니다
python -m pip freeze > requirements.txt
# 다른 환경에서 같은 구성을 설치합니다
python -m pip install -r requirements.txt
pip freeze는 직접 설치한 패키지뿐 아니라 그 의존성까지 모두 고정합니다. 배포용 애플리케이션에서는 이렇게 고정하는 편이 안전하고, 라이브러리는 범위를 넓게 열어 두는 편이 좋습니다. 이런 고정 작업을 더 체계적으로 해 주는 pip-tools, Poetry, uv 같은 도구도 있습니다.
pyproject.toml
pyproject.toml은 Python 프로젝트의 표준 설정 파일입니다. 프로젝트 이름과 버전, 의존성, 빌드 도구, 실행 명령을 한곳에 적습니다. 많은 개발 도구(pytest, Ruff, mypy 등)도 이 파일에서 설정을 읽습니다.
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "myapp"
version = "0.1.0"
description = "Small text utilities"
readme = "README.md"
dependencies = [
"requests",
]
[project.optional-dependencies]
dev = ["pytest", "mypy"]
[project.scripts]
myapp = "myapp.cli:main"
[build-system]은 패키지를 만들 빌드 백엔드를, [project]는 메타데이터와 의존성을 정합니다. 지원하는 Python 범위는 requires-python 항목으로 밝힐 수 있습니다. [project.scripts]에 적은 항목은 설치하면 터미널 명령이 됩니다. 개발 중에는 편집 가능 모드로 설치해 두면 소스를 고칠 때마다 다시 설치할 필요가 없습니다.
python -m pip install -e ".[dev]"
myapp # hello-python-world
배포 기초
내 패키지를 PyPI에 올리면 누구나 pip install로 설치할 수 있습니다. 순서는 다음과 같습니다.
pyproject.toml의 이름이 PyPI에서 아직 쓰이지 않았는지 확인합니다.build로 소스 배포본(sdist)과 휠(wheel)을 dist/ 폴더에 만듭니다.twine으로 먼저 연습용 TestPyPI에 올려 설치를 시험합니다.- 문제가 없으면 실제 PyPI에 올립니다. 인증은 비밀번호 대신 API 토큰을 씁니다.
python -m pip install build twine
python -m build
python -m twine upload --repository testpypi dist/*
python -m twine upload dist/*
정리
.py 파일 하나가 모듈이고, 모듈을 묶은 폴더가 패키지입니다.import, from ... import, 별칭, 상대 import를 상황에 맞게 쓰고 import *는 피합니다.- 애플리케이션의 의존성은
requirements.txt 등으로 고정해 재현할 수 있게 합니다. pyproject.toml에 프로젝트 정보를 모으고, build와 twine으로 배포합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.