Introduction
Jupyter notebook과 같은 대화형 개발 도구를 이용해 프로토타이핑을 하다 보면, 이미 임포트 해둔 모듈의 내용을 수정하고 싶을 때가 많다.
단순히 해당 임포트 구문을 다시 실행시키면 되지 않을까?
>>> import foo_module
>>> # foo_module.py의 코드 내용 변경...
>>> import foo_module
>>> # 변경 사항은 여전히 적용되지 않는다
아쉽게도 이미 한번 임포트 된 모듈은 다시 임포트 구문을 실행시킨다 해도 변경된 내용이 인터프리터 상에 적용되지는 않는다.
물론 노트북 커널이나 인터프리터를 재시작한 후 다시 임포트를 하면 변경 내용을 반영시킬 수 있다. 하지만 다시 실행시켰을 때 코드 테스트 환경을 똑같이 재현하기가 어려운 상황이라면, 최대한 현재 진행 상황을 유지한 채 모듈 수정 내용만을 반영시키고 싶을 것이다.
이런 상황에서 사용할 수 있는 것이 importlib 내장 라이브러리다.
importlib - The implementation of import
importlib은 사용자가 파이썬의 임포트 시스템과 상호작용하기 위한 API를 제공하는 내장 라이브러리이다.
이 라이브러리는 우리가 흔히 사용하고 있는 import 구문의 파이썬 구현과, 그 하위 콤포넌트들을 제공하고 있는데, importlib.__import__() 가 해당 구현으로 아무 인터프리터에서 import 문을 실행시킬 때 호출되는 __import__() 내장 함수를 파이썬 소스코드의 형태로 뜯어 보는 것이 가능하다. (더욱 단순화된 wrapper 함수 또한 제공하고 있다)
사용자는 import 함수의 구현 내용과, 실행에 필요한 하위 함수들을 이용해 필요에 맞는 임포터를 직접 구현하거나 임포트 관련 기능들을 자유롭게 사용할 수 있다.
마침 위에서 언급한 상황에 딱 맞는 함수도 제공하고 있는데, importlib.reload() 함수이다.
Solution: importlib.reload()
이 함수는 이전에 성공적으로 임포트 된 모듈을 말 그대로 reload 해 준다. 원하는 코드 위치에서 해당 함수를 실행시키면 모듈 코드가 재컴파일 되면서 모듈 레벨의 코드 또한 재실행되고, 파이썬 인터프리터나 IPython 커널을 종료하지 않고도 모듈의 수정 사항을 반영하는 것이 가능하다!
>>> import foo_module
>>> # foo_module.py의 코드 내용 변경...
>>> import importlib
>>> importlib.reload(foo_module)
>>> # 인터프리터 종료 없이도 변경 사항이 반영된다!
다만 유의할 점은 해당 모듈을 이미 참조하고 있던 다른 네임스페이스에서까지 자동으로 새 모듈로 바꿔놓아 주는 것은 아니라서, 만약 수정한 모듈을 참조하고 있는 다른 모듈 등이 있다면 일일이 리로드를 실행시켜 주어야 한다. 그리고 내장 모듈과 같이 두 번 이상 초기화 되는 상황을 염두에 두지 않은 모듈을 리로드하는 것도 지양해야 한다.
importlib 라이브러리를 소개한 김에 다른 유용한 함수들과 예시들도 추가적으로 알아보도록 하자.
importlib.import_module()
이 함수는 모듈을 프로그래밍 방식으로 임포트 할 때 사용되는 __import__() 의 래퍼 함수이다.
import importlib
typing = importlib.import_module("typing")
np = importlib.import_module("numpy")
import as 구문처럼 원하는 이름으로 임포트 하는 것도 물론 가능하다.
importlib.util.find_spec()
모듈의 스펙 (모듈을 로드하는 데 사용되는 임포트 관련 정보) 을 찾는 함수로, 임포트를 직접 수행하지 않고 모듈 임포트 가능 여부를 파악하는 데에 응용될 수 있다.
# https://docs.python.org/3/library/importlib.html 출처
import importlib.util
import sys
# For illustrative purposes.
name = 'itertools'
if name in sys.modules:
print(f"{name!r} already in sys.modules")
elif (spec := importlib.util.find_spec(name)) is not None:
# If you chose to perform the actual import ...
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
print(f"{name!r} has been imported")
else:
print(f"can't find the {name!r} module")
위 예시에서 importlib.util.find_spec() 구문이 실행되면 모듈 스펙이 검색되지 않는 상황에서는 None이 반환되고, 사용자는 이를 적절하게 이용할 수 있다.
importlib.invalidate_caches()
importlib.invalidate_caches() 함수는 임포트 할 모듈을 찾는 메타 경로 파인더의 캐시를 무효화 시킨다. 결과적으로는 코드 실행 중간에 새롭게 생겨난 모듈을 파악할 수 있게 되며, 코드 실행 중 모듈이 생성되거나 설치되는 경우에 사용할 수 있다.
>>> import importlib
>>> import foo_module
>>> # 코드 실행 중 새 모듈 생성
>>> new_module_name = foo_module.generate_new_module()
>>> # 캐시 무효화
>>> importlib.invalidate_caches()
>>> # 프로그램 시작 시 존재하지 않았던 모듈을 임포트 가능
>>> importlib.import_module(new_module_name)
importlib.util.LazyLoader
importlib.util.LazyLoader 클래스는 모듈이 모듈의 어트리뷰트에 접근하기 전까지 모듈 로더의 실행을 지연시키는 데 이용된다.
>>> # https://docs.python.org/3/library/importlib.html 출처
>>> import importlib.util
>>> import sys
>>> def lazy_import(name):
... spec = importlib.util.find_spec(name)
... loader = importlib.util.LazyLoader(spec.loader)
... spec.loader = loader
... module = importlib.util.module_from_spec(spec)
... sys.modules[name] = module
... loader.exec_module(module)
... return module
...
>>> lazy_typing = lazy_import("typing")
>>> # lazy_typing is a real module object,
>>> # but it is not loaded in memory yet.
>>> lazy_typing.TYPE_CHECKING
False
실제로 LazyLoader를 이용해 lazy import를 수행하는 경우, 실제로 어트리뷰트 접근 전까지는 모듈 컴파일과 모듈 코드 실행이 실제로는 일어나지 않는다. 따라서 lazy import를 수행한 이후 시점에서 소스코드가 변경되어도 실제 컴파일은 어트리뷰트 호출 시에 일어나기 때문에, 변경된 코드 내용대로 모듈이 작동하게 된다.
이런 식의 동적 임포트 느낌이 나는 구현은, 프로그램의 시작 시간을 단축시킬 수 있고 임포트 상호 의존성, 혹은 순환 의존성을 해결하는 방법으로 쓰일 수도 있다. (물론 loop 내에 있으면 임포트를 반복적으로 수행하는 동적 임포트보다는 더 나은 방법으로 보인다)
하지만 이런 식으로 임포트 실행을 미룰 경우, 예기치 못한 시점에 문맥을 벗어난 에러 메시지를 마주할 가능성이 커지기 때문에 안정성이 필요한 프로덕션 코드에서는 되도록이면 지양하는 것이 좋다.
Approximation of importlib.import_module()
공식 도큐멘테이션에서는 importlib.import_module() 의 간소화된 구현을 예시로써 제공하고 있다. 이 예시를 통해 임포트 함수의 구현 방식에 대해 파악할 수 있으며, importlib의 각종 API들이 어떤 식으로 작동하는 지를 보여주고 있어 사용자 정의 임포트 함수를 작성하는 데에 유용하게 쓰일 수 있다.
# https://docs.python.org/ko/3/library/importlib.html 출처
import importlib.util
import sys
def import_module(name, package=None):
"""An approximate implementation of import."""
absolute_name = importlib.util.resolve_name(name, package)
try:
return sys.modules[absolute_name]
except KeyError:
pass
path = None
if '.' in absolute_name:
parent_name, _, child_name = absolute_name.rpartition('.')
parent_module = import_module(parent_name)
path = parent_module.__spec__.submodule_search_locations
for finder in sys.meta_path:
spec = finder.find_spec(absolute_name, path)
if spec is not None:
break
else:
msg = f'No module named {absolute_name!r}'
raise ModuleNotFoundError(msg, name=absolute_name)
module = importlib.util.module_from_spec(spec)
sys.modules[absolute_name] = module
spec.loader.exec_module(module)
if path is not None:
setattr(parent_module, child_name, module)
return module
이번 글에서는 파이썬에서 기존에 임포트한 모듈을 프로그램 재시작 없이 재임포트 하는 방법과, importlib에 대해서 간단히 소개했다.
아래는 참고할 만한 사이트이다.
References
Python 3 importlib documentation
https://docs.python.org/3/library/importlib.html
Python 3 builtin import function