[Rejected] PEP 690 - Lazy Imports
원문 링크: PEP 690 - Lazy Imports
상태: Rejected 유형: Standards Track 작성일: 29-Apr-2022
PEP 690 – Lazy Imports (지연 임포트)
개요 (Abstract)
이 PEP는 임포트된 모듈의 검색 및 실행을 임포트된 객체가 처음 사용되는 순간까지 투명하게 지연시키는 기능을 제안합니다. Python 프로그램은 일반적으로 한 번의 실행에서 실제로 사용될 가능성이 있는 것보다 훨씬 더 많은 모듈을 임포트하기 때문에, 지연 임포트(lazy imports)는 로드되는 전체 모듈 수를 크게 줄여 시작 시간과 메모리 사용량을 개선할 수 있습니다. 지연 임포트는 또한 임포트 순환(import cycles)의 위험을 대부분 제거합니다.
상태: 이 PEP는 2022년 5월 3일자로 거부(Rejected)되었습니다.
동기 (Motivation)
일반적인 Python 코드 스타일은 모듈 수준에서 임포트를 선호합니다. 이는 임포트된 객체가 사용되는 각 스코프(scope) 내에서 반복할 필요가 없고, 런타임에 임포트 시스템이 반복적으로 실행되는 비효율성을 피하기 위함입니다. 그러나 이는 프로그램의 메인 모듈을 임포트할 때, 프로그램이 필요로 할 거의 모든 모듈이 즉시 연쇄적으로 임포트된다는 것을 의미합니다.
다수의 서브커맨드를 가진 Python 명령줄 프로그램(CLI)을 예로 들어봅시다. 각 서브커맨드는 다른 작업을 수행하며, 다른 의존성을 임포트해야 할 수 있습니다. 하지만 특정 프로그램 호출은 단일 서브커맨드만 실행하거나 전혀 실행하지 않을 수도 있습니다(예: --help
사용 정보만 요청한 경우). 이러한 프로그램에서 최상위(top-level)의 즉시 임포트(eager imports)는 전혀 사용되지 않을 많은 모듈을 임포트하게 되어, 해당 모듈을 (컴파일하고) 실행하는 데 소요되는 시간은 순전히 낭비가 됩니다.
시작 시간을 개선하기 위해 일부 대규모 Python CLI는 비용이 많이 드는 서브시스템의 임포트를 지연시키기 위해 수동으로 함수 내에 임포트 문을 인라인(inline)으로 배치하여 임포트를 지연시킵니다. 이 수동적인 접근 방식은 노동 집약적이고 취약합니다. 잘못 배치된 임포트 하나 또는 리팩토링(refactor)으로 인해 공들여 수행한 최적화 작업이 쉽게 무효화될 수 있습니다.
Python 표준 라이브러리에는 importlib.util.LazyLoader
를 통해 지연 임포트에 대한 내장 지원이 이미 포함되어 있습니다. demandimport
와 같은 서드파티 패키지도 존재합니다. 이러한 패키지들은 첫 번째 속성 접근 시 자체 임포트를 지연시키는 “지연 모듈 객체(lazy module object)”를 제공합니다. 그러나 이는 모든 임포트를 지연시키기에는 충분하지 않습니다. 예를 들어 from foo import a, b
와 같은 임포트는 foo
모듈에서 속성을 즉시 접근하므로 여전히 foo
모듈을 즉시 임포트합니다. 또한, __getattr__
또는 __getattribute__
와 같은 Python 수준의 구현이 필요하므로 모든 모듈 속성 접근에 눈에 띄는 런타임 오버헤드를 부과합니다.
과학 분야 Python 패키지 작성자들도 import scipy as sp
와 같이 작성한 다음 sp.linalg
와 같이 많은 다른 서브모듈에 쉽게 접근할 수 있도록 지연 임포트를 광범위하게 사용해왔습니다. 이는 많은 서브모듈을 미리 임포트할 필요 없이 가능하게 합니다. [SPEC 1]은 패키지 __init__.py
에서 명시적으로 사용할 수 있는 lazy_loader
라이브러리 형태로 이 관행을 성문화하여 지연적으로 접근 가능한 서브모듈을 제공합니다.
정적 타이핑(static typing) 사용자 또한 런타임에 전혀 사용되지 않을 수 있는 타입 어노테이션(type annotations)에 사용할 이름을 임포트해야 합니다 (PEP 563 또는 향후 PEP 649를 사용하여 어노테이션의 즉시 런타임 평가를 피하는 경우). 이 시나리오에서 지연 임포트는 불필요한 임포트의 오버헤드를 피하는 데 매우 매력적입니다.
이 PEP는 위의 모든 사용 사례를 포괄하고 실제 사용에서 감지 가능한 오버헤드를 부과하지 않는 보다 일반적이고 포괄적인 지연 임포트 솔루션을 제안합니다. 이 PEP의 구현은 실제 Python CLI에서 최대 70%의 시작 시간 개선과 최대 40%의 메모리 사용량 감소를 이미 입증했습니다.
지연 임포트는 또한 대부분의 임포트 순환(import cycles)을 제거합니다. 즉시 임포트의 경우, 임포트를 단순히 모듈 하단으로 이동하거나 함수 내에 인라인으로 배치하거나 from foo import bar
를 import foo
로 변경하여 해결되는 “가짜 순환(false cycles)”이 쉽게 발생할 수 있습니다. 지연 임포트의 경우, 이러한 “순환”은 단순히 작동합니다. 남게 될 유일한 순환은 실제로 두 모듈이 각각 다른 모듈의 이름을 모듈 수준에서 사용하는 경우이며, 이러한 “진정한 순환(true cycles)”은 관련된 클래스 또는 함수를 리팩토링해야만 해결할 수 있습니다.
근거 (Rationale)
이 기능의 목표는 임포트를 투명하게(transparently) 지연시키는 것입니다. “지연(lazy)”이란 모듈의 임포트(모듈 본문 실행 및 sys.modules
에 모듈 객체 추가)가 모듈(또는 모듈에서 임포트된 이름)이 실행 중에 실제로 참조될 때까지 발생하지 않아야 함을 의미합니다. “투명(transparent)”이란 지연된 임포트(그리고 지연된 임포트의 부작용 및 sys.modules
변경과 같은 필연적으로 관찰 가능한 효과) 외에 다른 관찰 가능한 동작 변경이 없다는 것을 의미합니다. 즉, 임포트된 객체는 정상적으로 모듈 네임스페이스에 존재하며, 처음 사용될 때 투명하게 로드됩니다. “지연 임포트된 객체(lazy imported object)”로서의 상태는 Python 코드나 C 확장 코드에서 직접 관찰할 수 없습니다.
임포트가 실제로 발생하기 전에도 임포트된 객체가 평소와 같이 모듈 네임스페이스에 존재해야 한다는 요구 사항은 아직 임포트되지 않은 객체를 나타내는 일종의 “지연 객체(lazy object)” 플레이스홀더(placeholder)가 필요하다는 것을 의미합니다. 투명성 요구 사항은 이 플레이스홀더가 Python 코드에 절대 보여서는 안 된다고 규정합니다. 이에 대한 모든 참조는 임포트를 트리거하고 실제 임포트된 객체로 대체해야 합니다.
Python (또는 C 확장) 코드가 모듈 __dict__
에서 직접 객체를 가져올 수 있다는 가능성을 고려할 때, 지연 객체의 우발적인 누출을 안정적으로 방지하는 유일한 방법은 딕셔너리 자체에서 조회 시 지연 객체 해결을 보장하도록 하는 것입니다.
조회 시 키가 지연 객체를 참조한다는 것을 발견하면, 반환하기 직전에 지연 객체를 즉시 해결합니다. 이터레이션(iteration) 도중에 딕셔너리를 변형하는 부작용을 피하기 위해, 딕셔너리의 모든 지연 객체는 이터레이션을 시작하기 전에 해결됩니다. 이는 대량 이터레이션(iter(dict)
, reversed(dict)
, dict.__reversed__()
, dict.keys()
, iter(dict.keys())
, reversed(dict.keys())
)을 사용할 때 성능 저하를 초래할 수 있습니다. 지연 객체를 전혀 포함하지 않는 대다수의 딕셔너리에서 이러한 성능 저하를 피하기 위해, 새로운 dk_lazy_imports
플래그를 위해 dk_kind
필드의 일부를 사용하여 딕셔너리가 지연 객체를 포함할 수 있는지 여부를 추적합니다.
이 구현은 지연 객체가 누출되는 것을 포괄적으로 방지하여, 어떤 용도로든 접근하기 전에 항상 실제 임포트된 객체로 해결되도록 보장하며, 일반적으로 딕셔너리에 대한 상당한 성능 영향을 피합니다.
명세 (Specification)
지연 임포트는 옵트인(opt-in) 방식이며, Python 인터프리터에 새 -L
플래그를 통해 또는 새로운 importlib.set_lazy_imports()
함수 호출을 통해 전역적으로 활성화할 수 있습니다. 이 함수는 두 개의 인자, 즉 불리언 enabled
와 excluding
컨테이너를 받습니다. enabled
가 True
이면 그 시점부터 지연 임포트가 켜집니다. False
이면 그 시점부터 꺼집니다. (excluding
키워드의 사용은 아래 “모듈별 옵트아웃(Per-module opt-out)” 섹션에서 논의됩니다.)
Python 인터프리터에 -L
플래그가 전달되면, 새로운 sys.flags.lazy_imports
가 True
로 설정되고, 그렇지 않으면 False
로 존재합니다. 이 플래그는 -L
을 새 Python 서브프로세스에 전파하는 데 사용됩니다.
sys.flags.lazy_imports
의 플래그는 지연 임포트의 현재 상태를 반드시 반영하는 것은 아니며, 단지 인터프리터가 -L
옵션으로 시작되었는지 여부만 나타냅니다. 특정 시점에 지연 임포트가 활성화되었는지 여부의 실제 현재 상태는 importlib.is_lazy_imports_enabled()
를 사용하여 검색할 수 있으며, 호출 시점에 지연 임포트가 활성화되어 있으면 True
를, 그렇지 않으면 False
를 반환합니다.
지연 임포트가 활성화되면, 모든 (그리고 오직) 최상위(top-level) 임포트의 로딩 및 실행은 임포트된 이름이 처음 사용될 때까지 지연됩니다. 이는 즉시 발생할 수도 있고 (예: 임포트 문 다음 줄에서), 훨씬 나중에 발생할 수도 있습니다 (예: 나중에 다른 코드에 의해 호출되는 함수 내에서 이름을 사용하는 동안).
이러한 최상위 임포트의 경우, 두 가지 컨텍스트(contexts)가 이를 즉시 임포트(eager)하게 만듭니다(지연되지 않음): try
/ except
/ finally
또는 with
블록 내의 임포트, 그리고 *
임포트 (from foo import *
). 예외 처리 블록 내의 임포트(이에는 예외를 “잡아” 처리할 수 있는 with
블록도 포함됨)는 임포트에서 발생하는 예외를 처리할 수 있도록 즉시 임포트됩니다. *
임포트는 어떤 이름이 네임스페이스에 추가되어야 하는지 알 수 있는 유일한 방법이 임포트를 수행하는 것이므로 즉시 임포트되어야 합니다.
클래스 정의 내 또는 함수/메서드 내의 임포트는 “최상위”가 아니므로 결코 지연되지 않습니다.
__import__()
또는 importlib.import_module()
를 사용하는 동적 임포트(Dynamic imports)도 결코 지연되지 않습니다.
지연 임포트 상태(즉, 활성화 여부 및 제외된 모듈; 아래 참조)는 인터프리터별이지만 인터프리터 내에서는 전역적입니다(즉, 모든 스레드에 영향을 미칩니다).
예시 (Example)
spam.py
라는 모듈이 있다고 가정해 봅시다:
# simulate some work
import time
time.sleep(10)
print("spam loaded")
그리고 spam
을 임포트하는 eggs.py
모듈:
import spam
print("imports done")
만약 python -L eggs.py
를 실행하면, spam
모듈은 (임포트 후에 전혀 참조되지 않으므로) 결코 임포트되지 않을 것이고, “spam loaded”는 결코 출력되지 않으며, 10초 지연도 없을 것입니다.
하지만 eggs.py
가 임포트 후에 spam
이름을 단순히 참조한다면, 그것으로 spam.py
의 임포트를 트리거하기에 충분합니다:
import spam
print("imports done")
spam # spam 이름을 참조
이제 python -L eggs.py
를 실행하면, 먼저 “imports done”이 출력되고, 10초 지연 후에 “spam loaded”가 출력되는 것을 볼 수 있습니다.
물론, 실제 사용 사례(특히 지연 임포트의 경우)에서는 이처럼 임포트 부작용(import side effects)에 의존하여 실제 작업을 트리거하는 것은 권장되지 않습니다. 이 예시는 단지 지연 임포트의 동작을 명확히 하기 위한 것입니다.
지연 임포트의 효과를 설명하는 또 다른 방법은 각 지연 임포트 문이 임포트된 이름의 각 사용 직전에 소스 코드에 인라인으로 작성된 것과 같다는 것입니다. 따라서 지연 임포트는 다음 코드를 변환하는 것과 유사하다고 생각할 수 있습니다:
import foo
def func1():
return foo.bar()
def func2():
return foo.baz()
다음과 같이:
def func1():
import foo
return foo.bar()
def func2():
import foo
return foo.baz()
이것은 지연 임포트 시 foo
의 임포트가 언제 발생할지에 대한 좋은 감각을 제공하지만, 지연 임포트가 이 코드 변환과 완전히 동일한 것은 아닙니다. 몇 가지 주목할 만한 차이점이 있습니다:
- 후자의 코드와 달리, 지연 임포트에서는
foo
이름이 여전히 모듈의 전역 네임스페이스에 존재하며, 이 모듈을 임포트하는 다른 모듈에 의해 임포트되거나 참조될 수 있습니다. (이러한 참조 또한 임포트를 트리거합니다.) - 지연 임포트의 런타임 오버헤드는 후자의 코드보다 훨씬 낮습니다. 임포트를 트리거하는
foo
이름에 대한 첫 번째 참조 이후에는 후속 참조에는 임포트 시스템 오버헤드가 전혀 없습니다. 이는 일반적인 이름 참조와 구별할 수 없습니다.
어떤 의미에서, 지연 임포트는 임포트 문을 임포트된 이름(들)의 단순한 선언으로 바꾸고, 나중에 참조될 때 완전히 해결되도록 합니다.
from foo import bar
스타일의 임포트도 지연될 수 있습니다. 임포트가 발생하면 bar
이름은 모듈 네임스페이스에 지연 임포트로 추가됩니다. bar
에 대한 첫 번째 참조는 foo
를 임포트하고 bar
를 foo.bar
로 해결합니다.
의도된 사용법 (Intended usage)
지연 임포트는 잠재적으로 호환성을 깨뜨릴 수 있는 의미론적(semantic) 변경이므로, 새 의미론 하에서 애플리케이션을 철저히 테스트하고, 예상대로 작동하는지 확인하며, 필요에 따라 특정 임포트를 옵트아웃(opt-out)할 준비가 된 Python 애플리케이션의 작성자 또는 유지보수자만이 활성화해야 합니다. 지연 임포트는 성공을 기대하며 Python 애플리케이션의 최종 사용자가 추측성으로 활성화해서는 안 됩니다.
애플리케이션에 지연 임포트를 활성화하는 애플리케이션 개발자는 애플리케이션이 올바르게 작동하기 위해 즉시 임포트되어야 하는 라이브러리 임포트를 옵트아웃할 책임이 있습니다. 라이브러리 작성자가 자신의 라이브러리가 지연 임포트 하에서도 정확히 동일하게 작동하도록 보장할 책임은 없습니다.
이 기능, -L
플래그 및 새로운 importlib
API의 문서는 의도된 사용법과 테스트 없이 채택할 경우의 위험에 대해 명확하게 설명해야 합니다.
구현 (Implementation)
지연 임포트는 내부적으로 “지연 임포트” 객체로 표현됩니다. import foo
또는 from foo import bar
와 같은 지연 임포트가 발생하면, 키 "foo"
또는 "bar"
가 모듈 네임스페이스 딕셔너리에 즉시 추가되지만, 그 값은 나중에 임포트를 실행하는 데 필요한 모든 메타데이터를 보존하는 내부 전용 “지연 임포트” 객체로 설정됩니다.
PyDictKeysObject
에 새로운 불리언 플래그(dk_lazy_imports
)가 설정되어 이 특정 딕셔너리가 지연 임포트 객체를 포함할 수 있음을 알립니다. 이 플래그는 딕셔너리에 지연 객체가 포함될 수 있을 때 “대량(bulk)” 작업에서 모든 지연 객체를 효율적으로 해결하기 위해서만 사용됩니다.
딕셔너리에서 키의 값을 추출하기 위해 키를 조회할 때마다, 해당 값이 지연 임포트 객체인지 확인됩니다. 만약 그렇다면, 지연 객체는 즉시 해결되고, 관련 임포트된 모듈이 실행되며, 지연 임포트 객체는 딕셔너리에서 실제 임포트된 값으로 (가능하다면) 대체되고, 해결된 값은 조회 함수에서 반환됩니다. 딕셔너리는 지연 임포트 객체를 해결하는 동안 임포트 부작용의 일부로 변형될 수 있습니다. 이 경우 키 값을 해결된 객체로 효율적으로 대체하는 것은 불가능합니다. 이 경우, 지연 임포트 객체는 해결된 객체에 대한 캐시된 포인터를 얻게 됩니다. 다음 접근 시 캐시된 참조가 반환되고 지연 임포트 객체는 딕셔너리에서 해결된 값으로 대체됩니다.
이 모든 것이 딕셔너리 구현에 의해 내부적으로 처리되기 때문에, 지연 임포트 객체는 모듈 네임스페이스에서 벗어나 Python 코드에 보이는 일이 절대 없습니다. 이러한 객체는 항상 첫 번째 참조에서 해결됩니다. 스텁(stub), 더미(dummy) 또는 썽크(thunk) 객체가 Python 코드에 보이는 일도 없고 sys.modules
에 배치되는 일도 없습니다. 모듈이 지연적으로 임포트되는 경우, 첫 번째 참조에서 실제로 임포트되기 전까지는 sys.modules
에 해당 항목이 전혀 나타나지 않습니다.
두 개의 다른 모듈(moda
와 modb
)이 모두 지연 임포트 foo
를 포함하는 경우, 각 모듈의 네임스페이스 딕셔너리는 키 "foo"
아래에 독립적인 지연 임포트 객체를 갖게 되며, 동일한 foo
모듈의 임포트를 지연시킵니다. 이것은 문제가 되지 않습니다. 예를 들어 moda.foo
가 처음 참조될 때, foo
모듈은 임포트되어 평소와 같이 sys.modules
에 배치되고, moda.__dict__["foo"]
키 아래의 지연 객체는 실제 foo
모듈로 대체됩니다. 이 시점에도 modb.__dict__["foo"]
는 여전히 지연 임포트 객체로 남아 있습니다. modb.foo
가 나중에 참조될 때, 또한 foo
를 임포트하려고 시도할 것입니다. 이 임포트는 Python에서 동일한 모듈의 후속 임포트에 대해 정상적인 것처럼 sys.modules
에 이미 존재하는 모듈을 찾을 것이고, 이 시점에서 modb.__dict__["foo"]
에 있는 지연 임포트 객체를 실제 foo
모듈로 대체할 것입니다.
지연 임포트 객체가 딕셔너리에서 벗어날 수 있는 두 가지 경우가 있습니다:
- 다른 딕셔너리로:
dict.update()
및dict.copy()
와 같은 대량 복사 작업의 성능을 유지하기 위해, 이들은 지연 임포트 객체를 확인하거나 해결하지 않습니다. 그러나 소스 딕셔너리에dk_lazy_imports
플래그가 설정되어 지연 객체를 포함할 수 있음을 나타내면, 해당 플래그는 업데이트/복사된 딕셔너리로 전달됩니다. 이는 지연 임포트 객체가 해결되지 않은 채 Python 코드로 누출되지 않도록 여전히 보장합니다. - 가비지 컬렉터(garbage collector)를 통해: 지연 임포트된 객체는 여전히 Python 객체이며 가비지 컬렉터 내에 존재합니다. 따라서
gc.get_objects()
등을 통해 수집되고 볼 수 있습니다. 이러한 방식으로 지연 객체가 Python 코드에 보이는 경우, 이는 불투명(opaque)하고 비활성(inert)입니다. 유용한 메서드나 속성이 없습니다. 이에 대한repr()
는<lazy_object 'fully.qualified.name'>
와 같이 표시될 것입니다.
지연 객체가 딕셔너리에 추가되면 dk_lazy_imports
플래그가 설정됩니다. 일단 설정되면, 딕셔너리의 모든 지연 임포트 객체가 해결되어야만 (예: 딕셔너리 이터레이션 시작 전에) 플래그가 지워집니다.
값과 관련된 모든 딕셔너리 이터레이션 메서드(dict.items()
, dict.values()
, PyDict_Next()
등)는 이터레이션을 시작하기 전에 딕셔너리의 모든 지연 임포트 객체를 해결하려고 시도합니다. (일부) 모듈 네임스페이스 딕셔너리만이 dk_lazy_imports
를 설정하므로, 딕셔너리 내의 모든 지연 임포트 객체를 해결하는 추가 오버헤드는 이를 필요로 하는 딕셔너리에서만 발생합니다. 일반적인 비지연 딕셔너리의 오버헤드를 최소화하는 것이 dk_lazy_imports
플래그의 유일한 목적입니다.
PyDict_Next
는 위치 0이 처음 접근될 때 모든 지연 임포트 객체를 해결하려고 시도하며, 해당 임포트는 예외와 함께 실패할 수 있습니다. PyDict_Next
는 예외를 설정할 수 없으므로, 이 경우 PyDict_Next
는 즉시 0을 반환하고, 모든 예외는 stderr
로 unraisable exception으로 출력됩니다.
이러한 이유로, 이 PEP는 PyDict_NextWithError
를 도입합니다. 이는 PyDict_Next
와 동일한 방식으로 작동하지만, 0을 반환할 때 오류를 설정할 수 있으며, 호출 후 PyErr_Occurred()
를 통해 확인해야 합니다.
try
/ except
/ with
블록 내 또는 클래스나 함수 본체 내의 임포트의 즉시성(eagerness)은 항상 즉시 임포트하는 새로운 EAGER_IMPORT_NAME
opcode를 통해 컴파일러에서 처리됩니다. 최상위 임포트는 -L
및/또는 importlib.set_lazy_imports()
에 따라 지연되거나 즉시 임포트될 수 있는 IMPORT_NAME
을 사용합니다.
디버깅 (Debugging)
python -v
를 통한 디버그 로깅은 임포트 문이 발견되었지만 임포트 실행이 지연될 때마다 로깅을 포함합니다.
Python의 임포트 비용 프로파일링을 위한 -X importtime
기능은 지연 임포트에 자연스럽게 적용됩니다. 프로파일링된 시간은 실제로 임포트하는 데 소요된 시간입니다.
지연 임포트 객체는 일반적으로 Python 코드에 보이지 않지만, 일부 디버깅의 경우 Python 코드에서 특정 딕셔너리의 특정 키에 있는 값이 지연 임포트 객체인지 해결을 트리거하지 않고 확인하는 것이 유용할 수 있습니다. 이를 위해 importlib.is_lazy_import()
를 사용할 수 있습니다:
from importlib import is_lazy_import
import foo
is_lazy_import(globals(), "foo")
foo
is_lazy_import(globals(), "foo")
이 예시에서, 지연 임포트가 활성화되어 있다면 is_lazy_import
에 대한 첫 번째 호출은 True
를 반환하고 두 번째 호출은 False
를 반환할 것입니다.
모듈별 옵트아웃 (Per-module opt-out)
아래에 언급된 하위 호환성 문제로 인해, 지연 임포트를 사용하는 애플리케이션은 일부 임포트를 즉시 임포트(eager)하도록 강제해야 할 수 있습니다.
퍼스트 파티(first-party) 코드에서는 try
또는 with
블록 내의 임포트가 결코 지연되지 않으므로, 다음 방법을 통해 쉽게 달성할 수 있습니다:
try:
# force these imports to be eager
import foo
import bar
finally:
pass
이 PEP는 새로운 importlib.eager_imports()
컨텍스트 매니저를 추가할 것을 제안하므로, 위의 기술은 더 간결해지고 의도를 명확히 하기 위한 주석이 필요하지 않습니다:
from importlib import eager_imports
with eager_imports():
import foo
import bar
컨텍스트 매니저 내의 임포트는 항상 즉시 임포트되므로, eager_imports()
컨텍스트 매니저는 널(null) 컨텍스트 매니저의 별칭일 수 있습니다. 컨텍스트 매니저의 효과는 전이적이지 않습니다. foo
와 bar
는 즉시 임포트되지만, 해당 모듈 내의 임포트는 여전히 일반적인 지연 규칙을 따릅니다.
수정하기 어려운 서드 파티 코드에서 임포트를 즉시 임포트하도록 강제해야 하는 더 어려운 경우가 발생할 수 있습니다. 이를 위해 importlib.set_lazy_imports()
는 두 번째 선택적 키워드 전용 excluding
인자를 받습니다. 이 인자는 모든 임포트가 즉시 임포트될 모듈 이름의 컨테이너로 설정할 수 있습니다:
from importlib import set_lazy_imports
set_lazy_imports(excluding=["one.mod", "another"])
이 효과 또한 얕습니다(shallow). one.mod
내의 모든 임포트는 즉시 임포트되지만, one.mod
가 임포트하는 모든 모듈 내의 임포트는 그렇지 않습니다.
set_lazy_imports()
의 excluding
매개변수는 모듈 이름을 포함하는지 여부를 확인하는 모든 종류의 컨테이너가 될 수 있습니다. 모듈 이름이 객체에 포함되어 있으면, 해당 모듈 내의 임포트는 즉시 임포트됩니다. 따라서 임의의 옵트아웃 로직을 __contains__
메서드에 인코딩할 수 있습니다:
import re
from importlib import set_lazy_imports
class Checker:
def __contains__(self, name):
return re.match(r"foo\.[^.]+\.logger", name)
set_lazy_imports(excluding=Checker())
Python이 -L
플래그로 실행되었다면, 지연 임포트는 이미 전역적으로 활성화되어 있을 것이고, set_lazy_imports(True, excluding=...)
를 호출하는 유일한 효과는 즉시 임포트될 모듈 이름/콜백을 전역적으로 설정하는 것입니다. set_lazy_imports(True)
가 excluding
인자 없이 호출되면, 제외 목록/콜백은 지워지고 모든 적격 임포트( try
/except
/with
블록에 없으며 import *
가 아닌 모듈 수준 임포트)는 그 시점부터 지연 임포트가 됩니다.
이 옵트아웃 시스템은 임포트의 지연성에 대한 지역적 추론 가능성(local reasoning)을 유지하도록 설계되었습니다. 주어진 임포트가 즉시 임포트될지 또는 지연 임포트될지 알기 위해서는 하나의 모듈 코드와 set_lazy_imports
의 excluding
인자(있는 경우)만 보면 됩니다.
테스트 (Testing)
CPython 테스트 스위트는 지연 임포트가 활성화된 상태에서 통과할 것입니다 (일부 테스트는 건너뜁니다). 하나의 빌드봇(buildbot)은 지연 임포트가 활성화된 상태에서 테스트 스위트를 실행해야 합니다.
C API
C 확장 모듈의 작성자를 위해 제안된 공개 C API는 다음과 같습니다:
C API | Python API |
---|---|
PyObject *PyImport_SetLazyImports(PyObject *enabled, PyObject *excluding) |
importlib.set_lazy_imports(enabled: bool = True, *, excluding: typing.Container[str] | None = None) |
int PyDict_IsLazyImport(PyObject *dict, PyObject *name) |
importlib.is_lazy_import(dict: typing.Dict[str, object], name: str) -> bool |
int PyImport_IsLazyImportsEnabled() |
importlib.is_lazy_imports_enabled() -> bool |
void PyDict_ResolveLazyImports(PyObject *dict) |
|
PyDict_NextWithError() |
void PyDict_ResolveLazyImports(PyObject *dict)
는 딕셔너리 내의 모든 지연 객체를 (있는 경우) 해결합니다. PyDict_NextWithError()
또는 PyDict_Next()
를 호출하기 전에 사용해야 합니다. PyDict_NextWithError()
는 PyDict_Next()
와 동일한 방식으로 작동하지만, 0을 반환하고 예외를 설정하여 호출자에게 오류를 전파한다는 점이 다릅니다. 호출자는 PyErr_Occurred()
를 사용하여 오류를 확인해야 합니다.
하위 호환성 (Backwards Compatibility)
이 제안은 기본적으로 기능이 비활성화되어 있을 때 완전한 하위 호환성을 유지합니다.
활성화되더라도 대부분의 코드는 (시작 시간 및 메모리 사용량 개선 외에는) 관찰 가능한 변경 없이 정상적으로 계속 작동할 것입니다. 네임스페이스 패키지(Namespace packages)는 영향을 받지 않습니다. 현재와 동일하게 작동하지만 지연적으로 작동합니다.
일부 기존 코드에서 지연 임포트는 현재 예상치 못한 결과와 동작을 초래할 수 있습니다. 기존 코드베이스에서 지연 임포트를 활성화할 때 발생할 수 있는 문제는 다음과 관련이 있습니다:
임포트 부작용 (Import Side Effects)
임포트 문 실행 중 임포트된 모듈의 실행으로 인해 발생하는 임포트 부작용은 임포트된 객체가 사용될 때까지 지연됩니다.
이러한 임포트 부작용에는 다음이 포함될 수 있습니다:
- 임포트 중에 부작용을 일으키는 로직을 실행하는 코드
- 임포트된 서브모듈이 부모 모듈에 속성으로 설정되는 것에 의존하는 코드
관련되고 전형적인 영향을 받는 사례는 Python 명령줄 인터페이스를 구축하기 위한 click
라이브러리입니다. 예를 들어 main.py
에 cli = click.group()
가 정의되어 있고, sub.py
가 main
에서 cli
를 임포트하여 데코레이터(@cli.command(...)
)를 통해 서브커맨드를 추가하지만, 실제 cli()
호출은 main.py
에 있다면, 지연 임포트가 서브커맨드 등록을 방해할 수 있습니다. 이는 Click
이 sub.py
임포트의 부작용에 의존하기 때문입니다. 이 경우 해결책은 importlib.eager_imports()
컨텍스트 매니저를 사용하는 등 sub.py
의 임포트가 즉시 임포트되도록 보장하는 것입니다.
동적 경로 (Dynamic Paths)
동적 Python 임포트 경로와 관련된 문제가 있을 수 있습니다. 특히, sys.path
에서 경로를 추가(그리고 임포트 후에 제거)하는 경우:
sys.path.insert(0, "/path/to/foo/module")
import foo
del sys.path[0]
foo.Bar()
이 경우, 지연 임포트가 활성화되면 foo
의 임포트는 sys.path
에 추가된 경로가 존재하는 동안 실제로 발생하지 않습니다.
이에 대한 쉬운 해결책(코드 스타일 개선 및 정리 보장도 겸함)은 sys.path
수정 사항을 컨텍스트 매니저 안에 두는 것입니다. 이는 with
블록 내의 임포트가 항상 즉시 임포트되므로 문제를 해결합니다.
지연된 예외 (Deferred Exceptions)
지연 임포트 중에 발생하는 예외는 일반 임포트 중의 예외와 마찬가지로 상위로 전파되어 부분적으로 구성된 모듈을 sys.modules
에서 지웁니다.
지연 임포트 중에 발생하는 오류는 임포트가 즉시 임포트될 때보다 나중에 발생하므로 (즉, 이름이 처음 참조되는 곳에서), 예외 처리기가 try
블록 내에서 임포트가 실행될 것으로 예상하지 않아 실수로 잡힐 수 있으며, 이는 혼란을 야기할 수 있습니다.
단점 (Drawbacks)
이 PEP의 단점은 다음과 같습니다:
- Python 임포트 동작에 미묘하게 비호환적인 의미론을 제공합니다. 이는 사용자로부터 두 가지 의미론을 모두 지원하도록 요청받을 수 있는 라이브러리 작성자에게 잠재적인 부담이며, Python 사용자/독자가 알아야 할 또 다른 가능성입니다.
- 일부 인기 있는 Python 코딩 패턴(특히 데코레이터로 채워지는 중앙 집중식 레지스트리)은 임포트 부작용에 의존하며, 지연 임포트와 함께 예상대로 작동하려면 명시적인 옵트아웃이 필요할 수 있습니다.
- 지연 임포트를 나타내는 이름에 접근하는 동안 언제든지 예외가 발생할 수 있으며, 이는 혼란과 예상치 못한 예외 디버깅으로 이어질 수 있습니다.
지연 임포트 의미론은 이미 Python 표준 라이브러리에서 가능하고 심지어 지원되므로, 이러한 단점은 이 PEP에 의해 새로 도입된 것이 아닙니다. 지금까지 일부 애플리케이션의 기존 지연 임포트 사용은 문제가 되지 않았습니다. 그러나 이 PEP는 지연 임포트 사용을 더 보편화하여 이러한 단점을 악화시킬 가능성이 있습니다.
이러한 단점은 이 PEP의 지연 임포트 구현이 제공하는 상당한 이점과 비교하여 고려되어야 합니다. 궁극적으로 이 기능이 널리 사용된다면 이러한 비용은 더 높아질 것입니다. 그러나 널리 사용된다는 것은 이 기능이 많은 가치를 제공한다는 것을 의미하며, 아마도 비용을 정당화할 것입니다.
보안 영향 (Security Implications)
코드의 지연된 실행은 임포트 문이 실행되는 시점과 임포트된 객체가 처음 참조되는 시점 사이에 프로세스 소유자, 셸 경로, sys.path
또는 기타 민감한 환경 또는 컨텍스트 상태가 변경되는 경우 보안 문제를 야기할 수 있습니다.
성능 영향 (Performance Impact)
참조 구현은 이 기능이 기존 실제 코드베이스(Instagram 서버, Meta의 여러 CLI 프로그램, Meta 연구원이 사용하는 Jupyter Notebook)에 미치는 성능 영향이 무시할 수 있는 반면, 시작 시간과 메모리 사용량에 상당한 개선을 제공한다는 것을 보여주었습니다.
참조 구현은 pyperformance
벤치마크 스위트에서 집계된 성능에 측정 가능한 변화를 보이지 않았습니다.
교육 방법 (How to Teach This)
이 기능은 옵트인(opt-in) 방식이므로, 초보자는 기본적으로 이 기능을 접하지 않을 것입니다. -L
플래그 및 importlib.set_lazy_imports()
에 대한 문서는 지연 임포트의 동작을 명확히 할 수 있습니다.
문서는 또한 지연 임포트를 선택하는 것은 Python 임포트에 대한 비표준 의미론을 선택하는 것이며, 이는 Python 라이브러리가 예상치 못한 방식으로 작동하지 않게 할 수 있음을 명확히 해야 합니다. 이러한 오류를 식별하고 옵트아웃(또는 지연 임포트 사용 중단)으로 해결할 책임은 전적으로 애플리케이션에 지연 임포트를 활성화하기로 선택한 사람에게 있으며, 라이브러리 작성자에게 있지 않습니다. Python 라이브러리는 지연 임포트 의미론을 지원할 의무가 없습니다. 비호환성을 정중하게 보고하는 것은 라이브러리 작성자에게 유용할 수 있지만, 그들은 단순히 자신의 라이브러리가 지연 임포트와 함께 사용되는 것을 지원하지 않는다고 말할 수 있으며, 이는 유효한 선택입니다.
발생할 수 있는 일부 문제를 처리하고 지연 임포트를 더 잘 활용하기 위한 몇 가지 모범 사례는 다음과 같습니다:
- 임포트 부작용에 의존하지 마십시오. 아마도 임포트 부작용에 대한 가장 일반적인 의존은 레지스트리 패턴일 것입니다. 이 패턴에서는 일부 외부 레지스트리의 채우기가 모듈 임포트 중에 암시적으로 발생하며, 종종 데코레이터를 통해 이루어집니다. 대신 레지스트리는 명시적으로 지정된 모듈에서 데코레이트된 함수 또는 클래스를 찾는 검색 프로세스를 수행하는 명시적인 호출을 통해 구축되어야 합니다.
- 항상 필요한 서브모듈을 명시적으로 임포트하십시오. 다른 임포트에 의존하여 모듈이 서브모듈을 속성으로 갖도록 하지 마십시오. 즉,
foo/__init__.py
에 명시적인from . import bar
가 없는 한, 항상import foo.bar; foo.bar.Baz
를 사용하고,import foo; foo.bar.Baz
는 사용하지 마십시오. 후자는foo.bar
가 다른 곳에서 임포트되는 부작용으로 인해 속성foo.bar
가 추가되기 때문에 (불안정하게) 작동할 뿐입니다. 지연 임포트에서는 이것이 항상 제때 발생하지 않을 수 있습니다. *
임포트는 항상 즉시 임포트되므로 사용을 피하십시오.
참조 구현 (Reference Implementation)
초기 구현은 Cinder의 일부로 제공됩니다. 이 참조 구현은 Meta 내부에서 사용되고 있으며, 일반적인 흐름에서 사용되지 않는 임포트를 실행할 필요가 없기 때문에 시작 시간(일부 애플리케이션의 경우 전체 런타임)을 40%-70% 개선하고, 메모리 사용량도 크게 줄이는(최대 40%) 것으로 입증되었습니다.
CPython 메인 브랜치를 기반으로 하는 업데이트된 참조 구현도 제공됩니다.
거부된 아이디어 (Rejected Ideas)
지연된 예외 감싸기 (Wrapping deferred exceptions)
혼란의 가능성을 줄이기 위해, 지연 임포트 실행 과정에서 발생하는 예외를 LazyImportError
예외( ImportError
의 서브클래스)로 대체하고, 원본 예외가 __cause__
로 설정될 수 있었습니다.
모든 지연 임포트 오류가 LazyImportError
로 발생하도록 보장하면, 실수로 잡히거나 다른 예상 예외로 오인될 가능성을 줄일 수 있었을 것입니다. 그러나 실제로 테스트 내에서와 같이 실패하는 모듈이 unittest.SkipTest
예외를 발생시키고 이것 또한 LazyImportError
로 감싸져 실제 예외 유형이 숨겨져 테스트가 실패하는 경우가 있었습니다. 여기서의 단점은 예상치 못한 지연된 예외가 실수로 잡히는 가상의 경우보다 더 큰 것으로 보입니다.
모듈별 옵트인 (Per-module opt-in)
__future__
임포트(예: from __future__ import lazy_imports
)를 사용한 모듈별 옵트인은 의미가 없습니다. 왜냐하면 __future__
임포트는 기능 플래그가 아니라 미래에 기본값이 될 동작으로의 전환을 위한 것이기 때문입니다. 지연 임포트가 기본 동작으로서 의미가 있을지는 불분명하므로, __future__
임포트로 이를 약속해서는 안 됩니다.
라이브러리가 특정 모듈에 대해 지연 임포트를 로컬하게 옵트인하기를 원하는 다른 경우가 있을 수 있습니다. 예를 들어, 대규모 라이브러리의 지연 최상위 __init__.py
는 서브컴포넌트를 지연 속성으로 접근 가능하게 만들기 위함입니다. 현재로서는 기능을 더 간단하게 유지하기 위해, 이 PEP는 “애플리케이션” 사용 사례에 초점을 맞추고 라이브러리 사용 사례는 다루지 않습니다. 이 PEP에 도입된 기본 지연 메커니즘은 미래에 이 사용 사례를 다루는 데도 사용될 수 있습니다.
개별 지연 임포트를 위한 명시적 구문 (Explicit syntax for individual lazy imports)
지연 임포트의 주요 목적이 오직 임포트 순환과 전방 참조(forward references)를 우회하는 것이었다면, 특정 대상 임포트를 지연시키기 위해 명시적으로 표시된 구문이 매우 합리적이었을 것입니다. 그러나 실제로는 코드베이스 내의 대부분의 임포트(및 서드 파티 의존성)를 지연 임포트 구문을 사용하도록 변환해야 하므로, 이 접근 방식으로는 강력한 시작 시간 또는 메모리 사용 이점을 얻기가 매우 어려웠을 것입니다.
주요 모듈에서 서브시스템의 최상위 임포트만 명시적으로 지연시키고, 서브시스템 내의 임포트는 모두 즉시 임포트되는 “얕은(shallow)” 지연을 목표로 할 수 있었습니다. 그러나 이는 극도로 취약합니다. 단 한 번의 잘못 배치된 임포트만으로도 신중하게 구축된 얕은 지연이 무효화될 수 있습니다. 반면에 전역적으로 지연 임포트를 활성화하는 것은 사용되는 임포트에 대해서만 비용을 지불하는 심층적인 강력한 지연을 제공합니다.
정적 타이핑과 같이 개별적으로 표시된 지연 임포트가 전방 참조를 피하기 위해 바람직하지만, 전역 지연 임포트의 성능/메모리 이점은 필요하지 않은 사용 사례가 있을 수 있습니다. 이는 동기가 되는 사용 사례가 다르고 새로운 구문이 필요하므로, 이 PEP에는 포함하지 않는 것을 선호합니다. 다른 PEP가 이 구현을 기반으로 추가 구문을 제안할 수 있습니다.
지연 임포트 활성화를 위한 환경 변수 (Environment variable to enable lazy imports)
환경 변수 옵트인은 이 기능의 오용을 너무 쉽게 유발합니다. Python 사용자가 자신이 실행하는 모든 Python 프로그램의 속도를 높일 목적으로 셸에 환경 변수를 전역적으로 설정하고 싶을 수 있습니다. 테스트되지 않은 프로그램에서 이러한 사용은 허위 버그 보고와 해당 도구 작성자에게 유지보수 부담을 초래할 가능성이 높습니다. 이를 피하기 위해 환경 변수 옵트인을 전혀 제공하지 않기로 선택합니다.
-L
플래그 제거 (Removing the -L flag)
저희는 -L
CLI 플래그를 제공합니다. 이 플래그는 이론적으로 python somescript.py
또는 python -m somescript
(Python 패키징 도구를 통해 배포되지 않는 경우)로 실행되는 개별 Python 프로그램을 실행하는 최종 사용자에 의해 유사한 방식으로 오용될 수 있습니다. 그러나 오용 가능성은 환경 변수보다 -L
에서 훨씬 적으며, -L
은 일부 애플리케이션에서 프로세스 시작부터 모든 임포트가 지연되도록 보장함으로써 시작 시간 이점을 극대화하는 데 가치가 있으므로 유지하기로 결정합니다.
의도하지 않은 명령줄 플래그(예: -s
, -S
, -E
, 또는 -I
)와 함께 임의의 Python 프로그램을 실행하는 것이 예상치 못하고 치명적인 결과를 초래할 수 있는 경우는 이미 존재합니다. -L
은 이 점에서는 새로운 것이 아닙니다.
절반 지연 임포트 (Half-lazy imports)
모듈 소스를 찾는 지점까지는 임포트 로더를 즉시 실행하되, 모듈의 실제 실행과 모듈 객체 생성을 지연시키는 것이 가능했을 것입니다. 이것의 장점은 특정 종류의 임포트 오류(예: 모듈 이름의 단순한 오타)가 임포트된 이름의 사용 시점까지 지연되지 않고 즉시 감지된다는 점입니다.
단점은 지연 임포트의 시작 시간 이점이 크게 줄어들 것이라는 점입니다. 사용되지 않는 임포트도 최소한 파일 시스템 stat()
호출을 필요로 할 것이기 때문입니다. 또한, 지연 임포트가 활성화될 때 어떤 임포트 오류는 즉시 발생하고 어떤 오류는 지연되는지에 대한 명확하지 않은 분할을 초래할 수도 있었습니다.
이 아이디어는 현재로서는 참조 구현에서 임포트 오타에 대한 혼란이 관찰된 문제가 아니었다는 점을 근거로 거부되었습니다. 일반적으로 지연된 임포트는 영원히 지연되지 않으며, 오류는 (임포트가 실제로 사용되지 않는 경우가 아니라면) 충분히 빨리 나타나 감지되고 수정됩니다.
절반 지연 임포트의 또 다른 동기는 모듈 자체가 어떤 플래그를 통해 지연적으로 임포트될지 또는 즉시 임포트될지를 제어하도록 허용하는 것이었을 것입니다. 이는 절반 지연 임포트가 필요하며, 임포트 지연의 성능 이점 중 일부를 포기하게 되기 때문입니다. 또한 일반적으로 모듈이 어떻게 또는 언제 임포트될지 결정하지 않고, 해당 모듈을 임포트하는 쪽이 결정하기 때문에 거부되었습니다. 이 PEP가 그러한 제어를 뒤집을 명확한 근거는 없습니다. 대신 임포트하는 코드가 결정을 내릴 수 있는 더 많은 옵션을 제공할 뿐입니다.
지연 동적 임포트 (Lazy dynamic imports)
__import__()
및/또는 importlib.import_module()
에 lazy=True
또는 유사한 옵션을 추가하여 지연 임포트를 수행할 수 있도록 하는 것이 가능했을 것입니다. 이 아이디어는 명확한 사용 사례가 부족하여 이 PEP에서 거부되었습니다. 동적 임포트는 이미 임포트에 대한 PEP 8 코드 스타일 권장 사항을 훨씬 벗어나 있으며, 코드 흐름의 원하는 지점에 배치하여 원하는 만큼 정확하게 지연시킬 수 있습니다. 이러한 임포트는 지연 임포트가 적용되는 모듈 최상위 수준에서는 일반적으로 사용되지 않습니다.
심층 즉시 임포트 재정의 (Deep eager-imports override)
제안된 importlib.eager_imports()
컨텍스트 매니저와 importlib.set_lazy_imports(excluding=...)
의 제외된 모듈은 모두 얕은(shallow) 효과를 가집니다. 즉, 적용된 위치에 대해서만 즉시 임포트를 강제하며 전이적(transitive)이지 않습니다. 둘 중 하나 또는 둘 다의 심층/전이적 버전을 제공하는 것이 가능했을 것입니다. 이 아이디어는 이 PEP에서 거부되었습니다. 구현이 복잡하고(스레드 및 비동기 코드 고려), 참조 구현 경험상 필요성이 입증되지 않았으며, 임포트의 지연성에 대한 지역적 추론을 방해하기 때문입니다.
심층 재정의는 혼란스러운 동작을 초래할 수 있습니다. 전이적으로 임포트된 모듈은 여러 위치에서 임포트될 수 있으며, 일부는 “심층 즉시 재정의”를 사용하고 일부는 그렇지 않기 때문입니다. 따라서 해당 모듈은 재정의가 없는 위치에서 처음 임포트되는 경우 여전히 지연적으로 임포트될 수 있습니다.
심층 재정의를 사용하면 주어진 임포트가 지연 임포트될지 또는 즉시 임포트될지에 대해 지역적으로 추론하는 것이 불가능합니다. 이 PEP에 명시된 동작을 사용하면 이러한 지역적 추론이 가능합니다.
지연 임포트를 기본 동작으로 만들기 (Making lazy imports the default behavior)
지연 임포트를 옵트인 방식 대신 Python 임포트의 기본/유일한 동작으로 만들면 장기적으로 몇 가지 이점이 있습니다. 라이브러리 작성자가 (결국) 두 가지 의미론을 모두 고려할 필요가 없어지기 때문입니다.
그러나 하위 호환성 문제로 인해 이는 __future__
임포트와 함께 장기간에 걸쳐서만 고려될 수 있습니다. 지연 임포트가 Python의 기본 임포트 의미론이 되어야 하는지는 전혀 명확하지 않습니다.
이 PEP는 지연 임포트를 기본 동작으로 고려하기 전에 Python 커뮤니티가 더 많은 경험을 쌓을 필요가 있다는 입장을 취하므로, 이는 전적으로 가능한 미래 PEP에 맡겨집니다.
저작권 (Copyright)
이 문서는 퍼블릭 도메인(public domain)에 공개되거나 CC0-1.0-Universal 라이선스 중 더 관대한 라이선스에 따라 제공됩니다.안녕하세요. Python 개발자이자 전문 기술 번역가로서 PEP 690 “Lazy Imports (지연 임포트)” 문서의 내용을 한국어 사용자가 이해하기 쉽게 번역하고 정리해 드립니다.
PEP 690 – Lazy Imports (지연 임포트)
상태: 이 PEP는 2022년 5월 3일자로 거부(Rejected)되었습니다.
1. 개요 (Abstract)
이 PEP는 임포트된 모듈의 검색 및 실행을, 해당 모듈 또는 임포트된 객체가 실제로 코드에서 처음 사용되는 시점까지 투명하게 지연시키는 기능을 제안합니다. Python 프로그램은 일반적으로 한 번의 실행에서 실제로 필요한 것보다 훨씬 더 많은 모듈을 임포트하는 경향이 있습니다. 따라서 지연 임포트(lazy imports)를 활용하면 로드되는 전체 모듈 수를 크게 줄여 프로그램의 시작 시간(startup time)을 단축하고 메모리 사용량(memory usage)을 개선할 수 있습니다. 또한, 지연 임포트는 임포트 순환(import cycles) 문제를 대부분 해소하는 데 기여합니다.
2. 동기 (Motivation)
대부분의 Python 코드는 모듈 수준에서 임포트를 선언하는 것을 선호합니다. 이는 임포트된 객체가 여러 스코프에서 사용될 때마다 반복적으로 임포트하는 것을 피하고, 런타임에 임포트 시스템이 불필요하게 여러 번 실행되는 비효율성을 방지하기 위함입니다. 하지만 이러한 방식은 프로그램의 메인 모듈이 실행될 때, 해당 프로그램이 잠재적으로 필요로 할 수 있는 거의 모든 모듈이 즉시 연쇄적으로 임포트되는 결과를 초래합니다.
예를 들어, 여러 서브커맨드를 가진 명령줄 인터페이스(CLI) 프로그램을 생각해봅시다. 특정 서브커맨드만 실행되는 경우, 다른 서브커맨드에서 필요한 수많은 모듈은 전혀 사용되지 않을 수 있습니다. 이 경우, 최상위(top-level)에서 이루어지는 즉시 임포트(eager imports)는 불필요한 모듈을 로드하고 실행하는 데 시간을 낭비하게 만듭니다.
일부 대규모 Python CLI 프로그램에서는 시작 시간을 개선하기 위해, 비용이 많이 드는 서브시스템의 임포트를 수동으로 함수 내에 인라인(inline)으로 배치하여 지연시키는 방법을 사용했습니다. 그러나 이러한 수동 접근 방식은 노동 집약적이며 오류에 취약하여, 사소한 코드 변경으로도 최적화 노력이 쉽게 무효화될 수 있습니다.
Python 표준 라이브러리에는 이미 importlib.util.LazyLoader
를 통한 지연 임포트 지원이 있으며, demandimport
와 같은 서드파티 패키지도 있습니다. 이들은 객체에 처음 접근할 때까지 임포트를 지연시키는 “지연 모듈 객체(lazy module object)”를 제공합니다. 하지만 from foo import a, b
와 같이 모듈에서 직접 속성에 접근하는 임포트의 경우 foo
모듈이 즉시 임포트되는 문제가 있습니다. 또한, __getattr__
또는 __getattribute__
와 같은 Python 수준의 구현이 필요하여 모든 모듈 속성 접근에 런타임 오버헤드를 유발합니다.
과학 분야 Python 패키지 개발자들은 import scipy as sp
와 같이 임포트한 후 sp.linalg
와 같이 다양한 서브모듈에 쉽게 접근할 수 있도록 지연 임포트를 광범위하게 사용해왔습니다. 이는 모든 서브모듈을 미리 임포트할 필요 없이 가능하게 합니다. [SPEC 1
]
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments