모듈 시스템 — import 방식, __name__, __all__
파이썬에서 **모듈(module)**은 .py 확장자를 가진 파이썬 파일 하나를 말합니다. 변수, 함수, 클래스를 파일로 분리해 재사용하고 코드를 체계적으로 관리하는 핵심 단위입니다.
모듈이란 무엇인가
모든 .py 파일은 모듈입니다. math.py를 만들면 그 자체가 math 모듈이 됩니다.
# mymodule.py
PI = 3.14159
def greet(name: str) -> str:
return f"안녕하세요, {name}님!"
class Calculator:
def add(self, a: int, b: int) -> int:
return a + b
import 방식 4가지
1. import 모듈명
import math
print(math.sqrt(16)) # 4.0
print(math.pi) # 3.141592653589793
네임스페이스가 명확하게 유지됩니다. 가장 권장되는 방식입니다.
2. from 모듈 import 이름
from math import sqrt, pi
print(sqrt(25)) # 5.0
print(pi) # 3.141592653589793
자주 사용하는 특정 항목만 가져올 때 유용합니다.
3. import 모듈 as 별칭
import numpy as np
import pandas as pd
arr = np.array([1, 2, 3])
긴 모듈명을 줄이거나, 충돌을 피할 때 사용합니다.
4. from 모듈 import * (권장하지 않음)
from math import * # 모든 공개 이름을 현재 네임스페이스로 가져옴
print(floor(3.7)) # 3
네임스페이스 오염이 발생하므로 프로덕션 코드에서는 피해야 합니다.
__name__ == "__main__" 패턴
파이썬 파일을 직접 실행하면 __name__ 변수가 "__main__"이 됩니다. 다른 파일에서 임포트되면 모듈명이 됩니다.
# calculator.py
def add(a: int, b: int) -> int:
return a + b
def subtract(a: int, b: int) -> int:
return a - b
if __name__ == "__main__":
# 직접 실행할 때만 동작하는 코드
print("Calculator 테스트:")
print(f"5 + 3 = {add(5, 3)}")
print(f"10 - 4 = {subtract(10, 4)}")
# 직접 실행: __name__ == "__main__" → 테스트 코드 실행
python calculator.py
# 임포트: __name__ == "calculator" → 테스트 코드 실행되지 않음
python -c "import calculator; print(calculator.add(1, 2))"
이 패턴은 모듈 테스트, CLI 진입점 분리, 스크립트와 라이브러리 겸용에 필수입니다.
__all__ — 공개 API 정의
__all__은 from 모듈 import * 시 노출할 이름 목록을 정의합니다. 또한 모듈의 의도적인 공개 API를 문서화하는 역할도 합니다.
# utils.py
__all__ = ["public_func", "PublicClass"]
def public_func():
"""공개 함수"""
return "공개"
def _helper():
"""내부 헬퍼 — 외부 노출 의도 없음"""
return "내부"
class PublicClass:
pass
class _InternalClass:
pass
from utils import *
public_func() # 정상 작동
# _helper() # NameError: _helper는 임포트되지 않음
__all__을 정의하지 않으면 언더스코어(_)로 시작하지 않는 모든 이름이 * 임포트 대상이 됩니다.
모듈 검색 경로: sys.path
파이썬은 import 시 다음 순서로 모듈을 찾습니다:
- 현재 실행 중인 스크립트의 디렉터리
PYTHONPATH환경 변수에 지정된 경로- 설치된 표준 라이브러리 경로
- site-packages (pip로 설치한 패키지)
import sys
# 모듈 검색 경로 확인
for path in sys.path:
print(path)
# 런타임에 검색 경로 추가
sys.path.insert(0, "/my/custom/lib")
import my_custom_module # 이제 찾을 수 있음
# 환경 변수로 경로 추가 (영구적)
# export PYTHONPATH="/my/custom/lib:$PYTHONPATH"
importlib으로 동적 임포트
문자열로 모듈명을 지정해 런타임에 임포트하는 방법입니다. 플러그인 시스템, 설정 기반 로더에서 활용합니다.
import importlib
# 문자열로 모듈 동적 임포트
module_name = "math"
math = importlib.import_module(module_name)
print(math.sqrt(9)) # 3.0
# 패키지 내 서브모듈 임포트
json = importlib.import_module("json")
data = json.loads('{"key": "value"}')
# 플러그인 시스템 예제
from importlib import import_module
def load_plugin(plugin_name: str):
"""플러그인 동적 로드"""
try:
module = import_module(f"plugins.{plugin_name}")
return module.Plugin()
except ModuleNotFoundError:
raise ValueError(f"플러그인 '{plugin_name}'을 찾을 수 없습니다.")
# 런타임에 플러그인 이름 결정
plugin = load_plugin("csv_exporter")
plugin.export(data)
# importlib.util로 파일 경로에서 직접 로드
import importlib.util
spec = importlib.util.spec_from_file_location(
"my_module", "/absolute/path/to/my_module.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
module.my_function()
순환 임포트 문제와 해결
두 모듈이 서로를 임포트하면 순환 임포트(circular import)가 발생합니다.
# a.py — 문제 상황
from b import func_b # b를 임포트
def func_a():
return "A"
# b.py — 문제 상황
from a import func_a # a를 임포트 → 순환!
def func_b():
return "B"
해결책 1: 임포트를 함수 내부로 이동
# a.py
def func_a():
return "A"
def combined():
from b import func_b # 함수 호출 시점에 임포트
return func_a() + func_b()
해결책 2: 공통 모듈로 분리
# common.py — 공통 의존성
def shared_utility():
return "공통"
# a.py
from common import shared_utility
# b.py
from common import shared_utility
해결책 3: 타입 힌트의 경우 TYPE_CHECKING 사용
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from b import ClassB # 타입 체크 시에만 임포트
def func_a(obj: "ClassB") -> None: # 문자열 어노테이션
pass
실전 예제: 설정 로더 모듈
# config_loader.py
"""설정 파일을 다양한 형식으로 로드하는 모듈"""
from __future__ import annotations
import importlib
import json
import os
from pathlib import Path
from typing import Any
__all__ = ["load_config", "ConfigLoader"]
def load_config(path: str | Path, format: str = "auto") -> dict[str, Any]:
"""설정 파일을 로드합니다."""
path = Path(path)
if format == "auto":
format = path.suffix.lstrip(".")
loader_map = {
"json": _load_json,
"toml": _load_toml,
"env": _load_env,
}
loader = loader_map.get(format)
if loader is None:
raise ValueError(f"지원하지 않는 형식: {format}")
return loader(path)
def _load_json(path: Path) -> dict[str, Any]:
with open(path, encoding="utf-8") as f:
return json.load(f)
def _load_toml(path: Path) -> dict[str, Any]:
try:
import tomllib # Python 3.11+
except ImportError:
tomllib = importlib.import_module("tomli") # 써드파티 fallback
with open(path, "rb") as f:
return tomllib.load(f)
def _load_env(path: Path) -> dict[str, str]:
result = {}
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#"):
key, _, value = line.partition("=")
result[key.strip()] = value.strip()
return result
class ConfigLoader:
"""설정 로더 클래스"""
def __init__(self, base_dir: str | Path = "."):
self.base_dir = Path(base_dir)
self._cache: dict[str, dict] = {}
def get(self, filename: str) -> dict[str, Any]:
if filename not in self._cache:
self._cache[filename] = load_config(self.base_dir / filename)
return self._cache[filename]
def reload(self, filename: str) -> dict[str, Any]:
self._cache.pop(filename, None)
return self.get(filename)
if __name__ == "__main__":
# 직접 실행 시 테스트
loader = ConfigLoader(".")
print("ConfigLoader 테스트 완료")
고수 팁
팁 1: importlib.resources로 패키지 내 리소스 접근
from importlib.resources import files
# 패키지 내 데이터 파일 읽기
data = files("mypackage").joinpath("data/config.json").read_text()
팁 2: sys.modules로 임포트 캐시 조작
import sys
# 이미 임포트된 모듈 목록
print("math" in sys.modules) # True (한 번이라도 임포트한 경우)
# 모듈 강제 재로드
import importlib
importlib.reload(sys.modules["mymodule"])
팁 3: __import__ 대신 importlib.import_module 사용
# 구식 방법 (권장하지 않음)
mod = __import__("math")
# 현대적 방법
import importlib
mod = importlib.import_module("math")
팁 4: 조건부 임포트 패턴
try:
import ujson as json # 빠른 JSON 라이브러리 시도
except ImportError:
import json # 표준 라이브러리 fallback
# 이제 json 변수로 통일해서 사용
data = json.loads('{"key": "value"}')