[Python] importlib: 동적 import와 메타데이터
정의
importlib는 import 시스템에 대한 프로그래밍 인터페이스. 동적 모듈 로딩, 메타데이터 조회, 진입점 발견, hot reload 등을 제공한다. 플러그인 시스템·테스트·동적 디스패치의 기반.
import 문은 importlib.__import__()의 syntax sugar.
import_module: 동적 import
import importlib
mod = importlib.import_module("os.path")
print(mod.join("/a", "b")) # '/a/b'
# 변수에서 모듈 이름
name = "json"
mod = importlib.import_module(name)
data = mod.loads('{"x": 1}')
# 점 표기로 서브모듈
mod = importlib.import_module("myapp.plugins.auth")
__import__()보다 깔끔. 플러그인 발견, 설정 기반 디스패치에 활용.
reload
import importlib
import my_module
# 코드 변경 후
importlib.reload(my_module)
REPL/Jupyter에서 코드 변경을 즉시 반영. 프로덕션에선 안 쓰는 게 좋음(미묘한 버그).
함정: 다른 모듈이 from my_module import X로 가져간 X는 갱신 안 됨. 부분 hot reload는 어려움.
find_spec, util
import importlib.util
spec = importlib.util.find_spec("numpy")
if spec is None:
print("numpy 없음")
else:
print(spec.origin) # 파일 경로
설치 여부 확인 후 분기:
HAS_NUMPY = importlib.util.find_spec("numpy") is not None
if HAS_NUMPY:
import numpy as np
else:
np = None
선택적 의존성 처리 패턴.
파일 경로로 import
import importlib.util
spec = importlib.util.spec_from_file_location(
"my_module", "/path/to/file.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
module.my_function()
플러그인 디렉터리 스캔 후 동적 로드.
importlib.metadata
설치된 패키지의 메타데이터 조회 (PEP 566, 3.8+).
from importlib.metadata import version, metadata, distributions, entry_points
print(version("requests")) # '2.32.0'
print(version("astropy")) # PackageNotFoundError if not installed
meta = metadata("requests")
print(meta["Name"], meta["Summary"])
# 설치된 모든 배포
for dist in distributions():
print(dist.metadata["Name"], dist.version)
이전 pkg_resources(setuptools)를 대체. 더 빠르고 stdlib에 있음.
version 자동 설정
# myapp/__init__.py
from importlib.metadata import version, PackageNotFoundError
try:
__version__ = version("myapp")
except PackageNotFoundError:
__version__ = "0.0.0+local"
pyproject.toml의 version과 자동 동기화.
entry_points: 플러그인 발견
다른 패키지가 등록한 진입점을 발견. 플러그인 아키텍처의 표준.
from importlib.metadata import entry_points
# 모든 entry_points
eps = entry_points()
# 특정 그룹
plugins = entry_points(group="myapp.plugins")
for ep in plugins:
plugin_class = ep.load() # 실제 객체 로드
plugin_class()
다른 패키지의 pyproject.toml:
[project.entry-points."myapp.plugins"]
auth = "myapp_auth:AuthPlugin"
cache = "myapp_cache:CachePlugin"
설치만 하면 자동 발견. pytest plugin, Click plugin, Sphinx extension 등 모두 이 메커니즘.
resources: 패키지 데이터 파일
importlib.resources (3.9+)는 패키지 내부의 데이터 파일 안전 접근.
from importlib.resources import files
# myapp/data/config.json 읽기
content = (files("myapp.data") / "config.json").read_text()
# binary
data = (files("myapp.data") / "logo.png").read_bytes()
기존 pkgutil.get_data나 __file__ 기반 접근보다 안전 (zip/wheel 안 패키지도 동작).
자주 보는 패턴
설정 기반 클래스 선택
import importlib
def get_backend(name):
module_path, class_name = name.rsplit(".", 1)
module = importlib.import_module(module_path)
return getattr(module, class_name)
config = {"backend": "myapp.backends.redis.RedisBackend"}
Backend = get_backend(config["backend"])
backend = Backend()
YAML/TOML 설정으로 클래스 지정 → 코드 수정 없이 백엔드 교체.
Django의 MIDDLEWARE = [...]이 같은 패턴.
플러그인 로더
import importlib
import pkgutil
def load_plugins(package):
"""package 안의 모든 모듈 로드."""
pkg = importlib.import_module(package)
for finder, name, ispkg in pkgutil.iter_modules(pkg.__path__):
full_name = f"{package}.{name}"
importlib.import_module(full_name)
load_plugins("myapp.plugins")
모듈 import 시 register() 호출 등 부작용으로 자동 등록.
Lazy import
# 무거운 의존성을 사용 시점에만
def visualize():
import matplotlib.pyplot as plt # 함수 호출 시 import
...
또는 importlib.util.LazyLoader:
import importlib.util
import sys
spec = importlib.util.find_spec("numpy")
loader = importlib.util.LazyLoader(spec.loader)
spec.loader = loader
module = importlib.util.module_from_spec(spec)
sys.modules["numpy"] = module
# 진짜 import는 첫 attribute 접근 시
대형 라이브러리(tensorflow, pytorch)는 부분만 쓸 때 시작 시간 단축.
동적 호출 가능 객체 로드
import importlib
def load_callable(path: str):
"""'module.path:func_name' 형태로 함수 로드."""
module_path, attr = path.split(":")
module = importlib.import_module(module_path)
return getattr(module, attr)
fn = load_callable("myapp.handlers:on_login")
fn(user_id)
Celery task, Flask route registration 등.
진입점 기반 CLI 플러그인
# 플러그인 패키지의 pyproject.toml
[project.entry-points."mycli.commands"]
deploy = "mycli_deploy:deploy_command"
# 메인 CLI
from importlib.metadata import entry_points
def main():
commands = entry_points(group="mycli.commands")
for ep in commands:
register_command(ep.name, ep.load())
설치만 하면 자동으로 CLI에 명령 추가.
함정
1. 순환 import
importlib.import_module도 일반 import와 같은 순환 함정 가짐. py-import-system에서 다룸.
2. reload의 한계
import my_module
from my_module import some_func
importlib.reload(my_module)
some_func() # 여전히 옛날 버전!
from X import Y는 이름 바인딩만 가져옴. reload 후엔 명시적 재 import 필요.
3. find_spec과 import side effects
spec = importlib.util.find_spec("numpy") # import는 안 됨 (모듈 객체 미생성)
# 그러나 일부 finder는 검색 중 부수효과 가능 (드물지만)
조건부 import는 try/except가 더 명확할 수 있음.
try:
import numpy
HAS_NUMPY = True
except ImportError:
HAS_NUMPY = False
4. 동적 import의 보안
mod = importlib.import_module(user_input) # 위험! 임의 코드 실행
사용자 입력 → 화이트리스트 검증 후만 사용.
ALLOWED = {"myapp.plugins.auth", "myapp.plugins.cache"}
def safe_import(name):
if name not in ALLOWED:
raise ValueError(f"forbidden: {name}")
return importlib.import_module(name)
모듈 캐시: sys.modules
import sys
print("os" in sys.modules) # True (한 번 import되면 캐시)
# 재 import 강제
del sys.modules["my_module"]
import my_module # 새로 로드 (그러나 다른 곳의 참조는 옛 것)
테스트에서 격리된 모듈 상태 필요 시 사용. 일반 코드는 만지지 말 것.
💬 댓글