파이썬의 비밀스러운 삶: Import 시스템
Source: Dev.to
파이썬이 코드를 찾는 방법 (그리고 왜 가끔 사라지는지)
Timothy는 의자에 몸을 기대고, 같은 문제를 3시간째 디버깅하고 있는 사람의 지친 좌절감으로 터미널을 바라보았다.
“이해가 안 돼요,” 라며 그는 스크립트를 백번 째 정도 실행했다. “파일을 삭제했어요.
old_utils.py를 프로젝트 디렉터리에서 물리적으로 지웠어요. 터미널도 다시 시작했고요. 그런데 코드를 실행하면…”import utils print(utils.get_version()) # Output: "Version 1.0 - DEPRECATED"“아직도 옛 버전을 로드하고 있어요! 파일은 더 이상 존재하지 않는데!”
Margaret가 다가와서 알듯한 미소를 지었다. “당신의 파이썬이 귀신에 씌어진 게 아니라, 기억력이 아주 좋을 뿐이에요. 당신은 import 시스템과 싸우고 있는데—규칙을 몰라서 지고 있는 거죠.”
캐시: sys.modules
import utils를 입력하면 파이썬은 먼저 무엇을 할까요?
“내 디렉터리에서
utils.py를 찾는 건가요?”
파이썬은 게으릅니다—가능하면 일을 피하려고 합니다. 파일 시스템을 검색하기 전에 sys.modules라는 사전을 확인합니다.
import sys
print(type(sys.modules)) # <class 'dict'>
print(len(sys.modules)) # e.g., 347 (varies)
# Show a few entries
for name in list(sys.modules.keys())[:10]:
print(name)
# sys, builtins, _frozen_importlib, _imp, _thread, ...
sys.modules는 모듈 이름을 모듈 객체에 매핑합니다. 'utils'가 이미 키로 존재한다면 파이썬은 파일 시스템에 손대지 않고 바로 그 캐시된 객체를 반환합니다.
import sys
import json
print('json' in sys.modules) # True
print(sys.modules['json']) # <module 'json' from '.../json/__init__.py'>
print(json.dumps({'test': 'data'})) # {"test": "data"}
print(dir(sys.modules['json'])[:5]) # ['JSONDecodeError', 'JSONDecoder', ...]
이 캐시는 프로세스가 살아 있는 동안 유지되므로, 모듈 소스 파일을 수정해도 인터프리터를 재시작하거나 캐시를 새로 고치기 전까지는 변경 사항이 보이지 않습니다.
문제 시연
# Create a simple module file
with open('example.py', 'w') as f:
f.write('''
def greet():
return "Hello, version 1"
''')
import example
print(example.greet()) # Hello, version 1
# Modify the file
with open('example.py', 'w') as f:
f.write('''
def greet():
return "Hello, version 2"
''')
# Re‑import (still cached)
import example
print(example.greet()) # Hello, version 1
print('example' in sys.modules) # True
모듈 다시 로드하기
위험한 방법: sys.modules에서 삭제 ❌
import sys
del sys.modules['example'] # Remove cached entry
import example # Reloads from disk
print(example.greet()) # Hello, version 2
왜 위험한가: 프로그램의 다른 부분이 아직 오래된 모듈 객체에 대한 참조를 가지고 있을 수 있어, 일관성 없는 동작을 초래합니다.
import sys
if 'example' in sys.modules:
del sys.modules['example']
# Write version 1
with open('example.py', 'w') as f:
f.write('def greet(): return "Version 1"')
import example as ex1
from example import greet as greet1
# Replace file with version 2 and delete cache
del sys.modules['example']
with open('example.py', 'w') as f:
f.write('def greet(): return "Version 2"')
import example as ex2
from example import greet as greet2
print(greet1()) # Version 1 (old reference)
print(greet2()) # Version 2
print(ex1 is ex2) # False
올바른 방법: importlib.reload() ✅
import importlib
import example
# Edit the file to version 2
with open('example.py', 'w') as f:
f.write('def greet(): return "Version 2"')
importlib.reload(example) # Updates the existing module object
print(example.greet()) # Version 2
print(id(example)) # Same memory address as before
reload()는 모듈을 제자리에서 업데이트하므로 기존 참조도 새로운 코드를 보게 됩니다. 하지만 reload 이전에 from example import greet와 같이 가져온 경우는 여전히 옛 함수 객체를 가리킵니다.
경로 지도: sys.path
모듈이 아직 캐시되지 않았다면 파이썬은 sys.path에 정의된 디렉터리 목록을 사용해 디스크에서 검색합니다.
import sys
print(sys.path)
보통 포함되는 항목은 다음과 같습니다:
- 파이썬 인터프리터를 실행한 스크립트가 위치한 디렉터리.
PYTHONPATH환경 변수에 지정된 경로(설정된 경우).- 표준 라이브러리 디렉터리.
- site‑packages(서드파티 라이브러리).
파이썬은 첫 번째로 일치하는 항목을 찾으면 바로 멈춥니다. sys.path를 조작해(예: 프로젝트의 src 폴더를 앞에 삽입) 어떤 버전의 모듈이 import될지 제어할 수 있습니다.
import sys
sys.path.insert(0, '/path/to/my/project')
import mymodule # 이제 파이썬은 '/path/to/my/project'를 가장 먼저 탐색합니다
핵심 정리:
sys.modules는 파이썬이 메모리 상에 유지하는 import된 모듈 캐시이며, import를 빠르게 하지만 오래된 코드가 남아 있을 수 있습니다. 안전하게 다시 로드하려면 importlib.reload()를 사용하고, 디스크에서 모듈을 찾을 때는 sys.path를 유념하세요. import 문제를 디버깅할 때는 항상 모듈이 이미 캐시되어 있는지 확인하는 것이 좋습니다.