PEP 810 – 명시적 지연 임포트
- Author:
- Pablo Galindo Salgado <pablogsal at python.org>, Germán Méndez Bravo <german.mb at gmail.com>, Thomas Wouters <thomas at python.org>, Dino Viehland <dinoviehland at gmail.com>, Brittany Reynoso <brittanyrey at gmail.com>, Noah Kim <noahbkim at gmail.com>, Tim Stumbaugh <me at tjstum.com>
- Discussions-To:
- Discourse thread
- Status:
- Final
- Type:
- Standards Track
- Created:
- 02-Oct-2025
- Python-Version:
- 3.15
- Post-History:
- 03-Oct-2025
- Resolution:
- 03-Nov-2025
Table of Contents
- 초록
- 동기
- 근거
- 사양
- 참조 구현
- 하위 호환성
- 보안 관련 사항
- 이 내용을 가르치는 방법
- 자주 묻는 질문
- 거부된 PEP 690과 어떻게 다릅니까?
- 실체화 시점에 무엇이 변경됩니까? 무엇이 동일하게 유지됩니까?
- 지연 임포트에서 오류가 발생하면 어떻게 됩니까?
- 지연 임포트는 임포트 시점의 부작용이 있는 모듈에 어떤 영향을 줍니까?
from ... import ...문과 함께 지연 임포트를 사용할 수 있습니까?lazy from module import Class는 전체 모듈을 로드합니까, 아니면 클래스만 로드합니까?- 타입 어노테이션과
TYPE_CHECKING임포트는 어떻게 됩니까? - 지연 임포트의 성능 오버헤드는 어느 정도입니까?
- 동일한 모듈을 지연 임포트와 즉시 임포트로 혼합하여 사용할 수 있습니까?
- 기존 코드를 지연 임포트를 사용하도록 어떻게 마이그레이션합니까?
- 스타 임포트(
from module import *)는 어떻습니까? - 지연 임포트는 임포트 훅 및 사용자 지정 로더와 어떻게 상호 작용합니까?
- 다중 스레드 환경에서는 어떻게 됩니까?
- 사용하지 않고도 지연 임포트의 실체화를 강제할 수 있습니까?
- 대신
importlib.util.LazyLoader를 사용하지 않는 이유는 무엇입니까? isort나black같은 도구가 중단됩니까?- 라이브러리가 지연 임포트와 호환되는지 어떻게 알 수 있습니까?
- 지연 임포트 모드를 전역적으로 활성화했는데 라이브러리가 올바르게 작동하지 않으면 어떻게 됩니까?
- 함수 내부에서 지연 임포트를 사용할 수 있습니까?
- 이전 Python 버전과의 향후 호환성은 어떻게 합니까?
- 명시적 지연 임포트는 PEP 649 및 PEP 749와 어떻게 상호 작용합니까?
- 지연 임포트는
dir(),getattr(), 모듈 인트로스펙션과 어떻게 상호 작용합니까? - 지연 임포트는 순환 임포트와 함께 작동합니까?
- 지연 임포트가 핫 패스의 성능에 영향을 줍니까?
sys.modules는 어떻습니까? 지연 임포트는 언제 그곳에 나타납니까?lazy from __future__ import feature가 작동합니까?lazy를 키워드 이름으로 선택한 이유는 무엇입니까?
- 보류된 아이디어
- 대안적 구현 아이디어
- 거부된 아이디어
- 새로운 동작을 기본값으로 만들기
__lazy_modules__ = ["*"]를 내장 구문으로 지원하기with블록 내부에서 지연 임포트 금지- 전역 플래그에서
with블록의 임포트를 즉시 임포트로 강제하기 - 딕셔너리 객체의 수정
__class__변이를 통한 지연 객체 변환- 모듈을 로드하지 않고
lazy임포트가 모듈을 찾게 하기 - from 임포트 중간에
lazy키워드 배치 - 임포트 문 끝에
lazy키워드 배치 - 명시적
eager키워드 추가 - 전역적으로 비활성화된 경우에도 필터가 지연 임포트를 강제하도록 허용
- 고급 기능에 밑줄 접두사가 붙은 이름 사용
- 지연 임포트에 데코레이터 구문 사용
- 새로운 소프트 키워드 대신 컨텍스트 관리자 사용
globals()에서 프록시 딕셔너리 반환__dict__또는globals()액세스 시 자동으로 실체화
- 감사의 말
- 각주
- Copyright
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 지연 임포트를 명시적 언어 기능으로 도입하기 위한 구문을 소개합니다:
lazy import json
lazy from json import dumps
지연 임포트는 임포트된 이름이 처음 사용될 때까지 모듈의 로딩과 실행을 지연합니다. 이는 임포트 문이 실행되는 시점에 모듈을 즉시 로드하고 실행하는 ‘일반’ 임포트와 대조됩니다.
개발자가 명시적 구문을 사용하여 개별 임포트를 지연 임포트로 표시할 수 있게 함으로써, Python 프로그램은 시작 시간, 메모리 사용량 및 불필요한 작업을 줄일 수 있습니다. 이는 명령줄 도구, 테스트 모음 및 대규모 의존성 그래프를 가진 애플리케이션에 특히 유용합니다.
이 제안은 완전한 하위 호환성을 유지합니다. 일반 임포트 문은 변경되지 않으며, 지연 임포트는 명시적으로 요청된 곳에서만 활성화됩니다.
동기
Python 코드에서 가장 일반적인 관례는 모든 임포트를 모듈 수준에, 일반적으로 파일의 시작 부분에 배치하는 것입니다. 이렇게 하면 반복을 피하고 임포트 의존성을 명확하게 하며, 모듈당 임포트 문을 한 번만 평가하여 런타임 오버헤드를 최소화할 수 있습니다.
이 접근 방식의 주요 단점은 Python 실행을 위해 첫 번째 모듈(“main” 모듈)을 임포트할 때 즉각적인 임포트 연쇄가 발생하는 경우가 많고, 사용되지 않을 수도 있는 많은 의존성을 낙관적으로 로드한다는 점입니다. 이러한 영향은 여러 하위 명령을 제공하는 명령줄 도구에서 특히 큰 비용을 초래합니다. --help 옵션으로 명령을 실행하는 것만으로도 수십 개의 불필요한 모듈을 로드하고 몇 초가 걸릴 수 있습니다. 이 기본 예제는 프로그램을 실행하는 방법에 대해 사용자에게 유용한 피드백을 제공하는 데 필요한 항목이 무엇인지 보여 줍니다. 비효율적으로도 사용자는 원하는 명령을 파악하고 프로그램을 “실제로” 호출할 때 이 오버헤드를 다시 부담합니다.
임포트를 지연하는 상당히 일반적인 방법은 임포트를 함수 내부로 옮기는 것(인라인 임포트)입니다. 그러나 이 방식은 구현하고 유지 관리하는 데 더 많은 작업이 필요하며, 단 한 번의 부주의한 최상위 수준 임포트로 무효화될 수 있습니다. 또한 모듈의 전체 의존성 집합을 모호하게 만듭니다. Python 표준 라이브러리를 분석한 결과, 테스트를 제외한 모든 임포트 중 약 17%(730개 파일에 걸친 전체 약 3500개의 임포트)가 실행을 지연하기 위한 목적으로 이미 함수나 메서드 내부에 배치되어 있습니다. 이는 개발자들이 이미 성능에 민감한 코드에서 지연 임포트를 수동으로 구현하고 있음을 보여 줍니다. 그러나 이렇게 하려면 코드베이스 전체에 임포트를 분산해야 하며, 전체 의존성 그래프를 한눈에 이해하기가 더 어려워집니다.
표준 라이브러리는 이러한 비효율성 문제 중 일부를 해결하기 위해 LazyLoader 클래스를 제공합니다. 모듈 수준의 임포트가 인라인 임포트와 대부분 비슷하게 작동하도록 허용합니다. 많은 과학 Python 라이브러리가 유사한 패턴을 채택했으며, 이는 SPEC 1 에서 공식화되었습니다. 서드파티 lazy_loader 패키지도 있으며, 이는 지연 임포트를 구현하는 또 다른 방식입니다. 정적 타입 검사에만 사용되는 임포트도 잠재적으로 불필요한 임포트의 또 다른 원천이며, 오버헤드를 최소화하기 위한 방식 역시 서로 다양합니다. 여기서 즉시 임포트를 지연하거나 제거하기 위해 사용되는 다양한 접근 방식은 일반적인 지연 임포트 메커니즘의 모든 잠재적 사용 사례를 포괄하지 못합니다. 명확한 표준은 없으며, 예상치 못한 위치에서의 런타임 오버헤드나, 더 나쁘게는 런타임 인트로스펙션을 포함한 여러 단점이 있습니다.
이 제안은 지역적이고 명시적이며 제어 가능하고 세분화된 설계를 통해 지연 임포트를 위한 구문을 도입합니다. 이러한 각각의 특성은 이 기능을 실제로 예측 가능하고 안전하게 사용할 수 있도록 하는 데 필수적입니다.
동작은 local입니다. 지연성은 lazy 키워드로 표시된 특정 임포트에만 적용되며, 다른 임포트로 재귀적으로 연쇄되지 않습니다. 이를 통해 개발자는 임포트된 모듈 자체의 동작이 달라질지 걱정하지 않고, 눈앞의 코드 한 줄만 살펴보면서 지연성의 영향을 추론할 수 있습니다. lazy import 는 사용할 때마다 독립적으로 내리는 결정이지, 의미론을 전역적으로 변경하는 것이 아닙니다.
의미론은 명시적입니다. 이름을 지연 임포트하면 임포트하는 모듈에 바인딩이 즉시 생성되지만, 대상 모듈은 해당 이름에 처음으로 접근할 때까지 로드되지 않습니다. 이 시점 이후에는 해당 바인딩이 일반 임포트로 생성된 바인딩과 구별되지 않습니다. 이러한 명확성은 예기치 않은 동작을 줄이고, Python의 임포트 메커니즘에 깊이 익숙하지 않은 개발자도 이 기능을 쉽게 사용할 수 있게 합니다.
지연 임포트는 지연 로딩이 임포트하는 코드 자체에 의해서만 트리거된다는 의미에서 통제된방식입니다. 일반적으로 라이브러리는 자체 작성자가 지연 임포트로 표시하기로 선택한 경우에만 지연 임포트를 사용하게 됩니다. 이를 통해 하위 사용자에게 책임이 전가되는 것을 피하고 라이브러리 동작에서 우발적인 예기치 않은 동작이 발생하는 것을 방지합니다. 라이브러리 작성자는 일반적으로 자체 임포트 하위 그래프를 관리하므로, 지연성이 언제 어떻게 적용되는지 예측 가능한 방식으로 통제할 수 있습니다.
이 메커니즘은 또한 세분화된방식입니다. 이는 전역 플래그나 암시적 설정이 아니라 개별 임포트에 대한 명시적 구문을 통해 도입됩니다. 이를 통해 개발자는 코드베이스에서 성능에 가장 민감한 영역부터 시작하여 이 기능을 점진적으로 도입할 수 있습니다. 이 기능을 커뮤니티에 도입하면서, 온보딩 경험을 선택적이고 점진적이며 각 프로젝트의 요구 사항에 맞게 조정할 수 있도록 만들고자 합니다.
지연 임포트는 다음과 같은 몇 가지 구체적인 이점을 제공합니다:
- 명령줄 도구는 사용자가 직접 호출하는 경우가 많으므로 지연 시간 – 특히 시작 지연 시간 – 이 상당히 두드러집니다. 이러한 프로그램은 일반적으로 수명이 짧은 프로세스이기도 합니다(예: 웹 서버와는 대조적입니다). 지연 임포트를 사용하면 실제로 도달한 코드 경로만 모듈을 임포트합니다. 이를 통해 실제 환경에서 시작 시간을 50-70% 단축할 수 있으며, 일반적인 사용자 경험을 크게 개선하고 빠른 시작이 가장 중요한 영역에서 Python의 경쟁력을 높일 수 있습니다.
- 타입 어노테이션에는 런타임에 전혀 사용되지 않는 임포트가 필요한 경우가 많습니다. 일반적인 우회 방법은 해당 임포트를
if TYPE_CHECKING:블록 [1] 안에 넣는 것입니다. 지연 임포트를 사용하면 어노테이션 전용 임포트에 런타임 비용이 발생하지 않으므로 이러한 가드가 필요하지 않으며, 어노테이션이 적용된 코드베이스를 더 깔끔하게 만들 수 있습니다. - 대규모 애플리케이션은 수천 개의 모듈을 임포트하는 경우가 많고, 각 모듈은 함수 객체와 타입 객체를 생성하여 메모리 비용을 발생시킵니다. 수명이 긴 프로세스에서는 이로 인해 기준 메모리 사용량이 눈에 띄게 증가합니다. 지연 임포트는 모듈이 필요할 때까지 이러한 비용을 미루어 사용되지 않는 하위 시스템을 로드하지 않은 상태로 유지합니다. 실제 워크로드에서 30-40%의 메모리 절감이 관찰되었습니다.
근거
이 제안의 설계는 명확성, 예측 가능성, 도입 용이성을 중심으로 합니다. 각 결정은 언어나 런타임에 불필요한 복잡성을 도입하지 않으면서 지연 임포트가 실질적인 이점을 제공하도록 하기 위해 이루어졌습니다.
또한 이 PEP가 하나의 구체적인 접근법을 제시하지만, 제안의 일부 핵심 측면과 의미론에 대해서는 대체 구현 전략도 나열한다는 점에 주목할 필요가 있습니다. 커뮤니티가 동일한 핵심 의미론을 유지하는 다른 기술적 경로를 강하게 선호하거나 특정 선택지에 대해 근본적인 의견 차이가 있는 경우를 대비하여, 이 제안을 준비하면서 이미 완료한 브레인스토밍 내용을 참고 자료로 포함했습니다.
새로운 lazy 키워드를 도입하기로 한 선택은 명시적 구문이 필요하다는 점을 반영합니다. 지연 임포트는 일반 임포트와 의미가 다릅니다. 오류와 부작용이 임포트 문이 실행될 때가 아니라 처음 사용될 때 발생합니다. 이러한 의미상의 차이 때문에 지연성은 전역 설정이나 멀리 떨어진 모듈 수준 선언에 숨겨지지 않고 임포트 위치 자체에 드러나야 합니다. lazy 키워드는 임포트 동작을 국소적으로 추론할 수 있게 하므로, 임포트가 지연되는지 파악하기 위해 코드의 다른 부분을 검색할 필요가 없습니다. 나머지 임포트 의미는 변경되지 않습니다. 동일한 임포트 메커니즘, 모듈 검색 및 로딩 메커니즘이 사용됩니다.
또 다른 중요한 결정은 딕셔너리 조회를 수정하는 대신 모듈의 네임스페이스에서 지연 임포트를 프록시 객체로 표현하는 것입니다. 이전 접근법에서는 딕셔너리에 지연성을 포함하는 방법을 실험했지만, 이로 인해 추상화가 모호해지고 런타임의 관련 없는 부분에 영향을 줄 위험이 있었습니다. 딕셔너리는 Python의 기본적인 데이터 구조입니다 – 말 그대로 모든 객체가 딕셔너리 위에 구축됩니다 – 따라서 딕셔너리에 후크를 추가하면 중요한 최적화를 방해하고 전체 런타임을 복잡하게 만들 수 있습니다. 프록시 접근법은 더 단순합니다. 처음 사용될 때까지 자리 표시자처럼 동작하며, 그 시점에 임포트를 해결하고 이름을 다시 바인딩합니다. 그 이후에는 해당 바인딩을 일반 임포트와 구별할 수 없습니다. 따라서 이 메커니즘은 설명하기 쉽고 인터프리터의 나머지 부분은 변경되지 않은 상태로 유지됩니다.
라이브러리 작성자의 하위 호환성도 중요한 관심사였습니다. 많은 유지 관리자는 Python의 새 버전과 이전 버전을 동시에 지원할 수 있는 마이그레이션 경로를 필요로 합니다. 이러한 이유로 제안에는 과도기적 메커니즘으로 __lazy_modules__ 전역 변수가 포함됩니다. 모듈은 어떤 임포트를 지연 임포트로 처리할지(모듈 이름을 문자열로 나열하여) 선언할 수 있으며, Python 3.15 이상에서는 해당 임포트가 lazy 키워드로 임포트된 것처럼 자동으로 지연 임포트가 됩니다. 이전 버전에서는 이 선언이 무시되므로 임포트는 즉시 수행됩니다. 이를 통해 작성자는 해당 키워드를 표준 구문으로 신뢰할 수 있을 때까지 실용적인 연결 수단을 확보할 수 있습니다.
마지막으로 이 기능은 점진적으로 채택되도록 설계되었습니다. 개발자가 명시적으로 선택하지 않는 한 아무것도 변경되지 않으며, 성능에 민감한 영역의 몇몇 임포트부터 채택을 시작할 수 있습니다. 이는 Python의 점진적 타이핑 경험을 반영합니다. 즉, 첫날부터 프로젝트가 전역적으로 채택하도록 강제하지 않고 점진적으로 도입할 수 있는 메커니즘입니다. 특히 채택은 “외부에서 내부로” 진행할 수도 있으므로, CLI 작성자는 해당 도구가 사용할 수 있는 모든 라이브러리를 변경하지 않고도 지연 임포트를 도입하여 사용자 대면 도구의 속도를 높일 수 있습니다.
기타 설계 결정
- 지연성의 범위는 의도적으로 국소적이며 재귀적이지 않습니다. 지연 임포트는 해당 임포트가 나타나는 특정 문에만 영향을 주며, 다른 모듈이나 서브모듈로 연쇄되지 않습니다. 이러한 선택은 예측 가능성을 위해 매우 중요합니다. 개발자가 코드를 읽을 때 의존성 그래프의 더 깊은 곳에 숨겨진 지연성을 걱정하지 않고 한 줄씩 임포트 동작을 추론할 수 있습니다. 그 결과 강력하면서도 컨텍스트 안에서 쉽게 이해할 수 있는 기능이 제공됩니다.
- 또한 인터프리터에서 실행되는 모든 코드에 대해 지연 임포트를 활성화하거나 비활성화하는 메커니즘을 제공하는 것이 유용합니다(이 PEP에서는 이를 ‘전역 지연 임포트 플래그’라고 합니다). 기본 설계는 명시적인
lazy import구문을 중심으로 하지만, 대규모 애플리케이션, 테스트 환경 또는 프레임워크와 같이 여러 모듈에서 지연성을 일관되게 활성화하는 것이 가장 큰 이점을 제공하는 상황도 있습니다. 전역 스위치를 사용하면 일관된 동작을 쉽게 실험하거나 강제할 수 있으며, 제외 항목이나 도구별 설정을 준수하도록 필터링 API와 함께 사용할 수도 있습니다. 이를 통해 유연성이나 제어력을 줄이지 않고도 전역적 채택을 실용적으로 진행할 수 있습니다.
사양
문법
새로운 소프트 키워드 lazy가 추가됩니다. 소프트 키워드는 특정 문법적 컨텍스트에서만 특별한 의미를 가지는 컨텍스트 의존 키워드입니다. 그 밖의 곳에서는 일반 식별자(예: 변수 이름)로 사용할 수 있습니다. lazy 키워드는 import 문 앞에 나타날 때만 특별한 의미를 가집니다.
import_name:
| 'lazy'? 'import' dotted_as_names
import_from:
| 'lazy'? 'from' ('.' | '...')* dotted_name 'import' import_from_targets
| 'lazy'? 'from' ('.' | '...')+ 'import' import_from_targets
구문 제한
소프트 키워드는 전역(모듈) 수준에서만 허용되며, 함수, 클래스 본문, try 블록 또는 import * 내부에는 허용되지 않습니다. 소프트 키워드를 사용하는 import 문은 potentially lazy 상태가 될 수 있습니다. lazy가 될 수 없는 import는 전역 lazy imports 플래그의 영향을 받지 않으며, 대신 항상 즉시 수행됩니다. 또한 from __future__ import 문은 lazy가 될 수 없습니다.
구문 오류의 예:
# SyntaxError: lazy import not allowed inside functions
def foo():
lazy import json
# SyntaxError: lazy import not allowed inside classes
class Bar:
lazy import json
# SyntaxError: lazy import not allowed inside try/except blocks
try:
lazy import json
except ImportError:
pass
# SyntaxError: lazy from ... import * is not allowed
lazy from json import *
# SyntaxError: lazy from __future__ import is not allowed
lazy from __future__ import annotations
의미론
lazy 키워드를 사용하면 import가 potentially lazy 상태가 됩니다(Lazy imports filter를 참조하면 고급 재정의 메커니즘을 확인할 수 있습니다). 모듈은 import 문에서 즉시 로드되지 않습니다. 대신 lazy 프록시 객체가 생성되어 이름에 바인딩됩니다. 실제 모듈은 해당 이름을 처음 사용할 때 로드됩니다.
lazy from ... import를 사용하면 각 가져온 이름이 lazy 프록시 객체에 바인딩됩니다. 이러한 이름 중 어떤 이름이든처음 액세스하면 전체 모듈의 로딩이 트리거되며, 해당 특정 이름만실제 값으로 구체화됩니다. 다른 이름은 액세스할 때까지 lazy 프록시로 남아 있습니다. 인터프리터의 적응형 특수화는 몇 차례 액세스한 후 lazy 검사를 제거하도록 최적화합니다.
lazy import를 사용하는 예:
import sys
lazy import json
print('json' in sys.modules) # False - module not loaded yet
# First use triggers loading
result = json.dumps({"hello": "world"})
print('json' in sys.modules) # True - now loaded
lazy from ... import를 사용하는 예:
import sys
lazy from json import dumps, loads
print('json' in sys.modules) # False - module not loaded yet
# First use of 'dumps' triggers loading json and reifies ONLY 'dumps'
result = dumps({"hello": "world"})
print('json' in sys.modules) # True - module now loaded
# Accessing 'loads' now reifies it (json already loaded, no re-import)
data = loads(result)
모듈은 전역 범위에 __lazy_modules__ 변수를 정의할 수 있으며, 이 변수는 어떤 모듈 이름을 potentially lazy 상태로 만들어야 하는지(lazy 키워드를 사용한 것처럼) 지정합니다. 이 변수는 각 import 문에서 import를 potentially lazy 상태로 만들지 결정하기 위해 확인됩니다. 이 확인은 가져오는 완전 수식 모듈 이름이 포함된 문자열을 사용하여 __lazy_modules__ 객체에서 __contains__를 호출하는 방식으로 수행됩니다. 일반적으로 __lazy_modules__는 완전 수식 모듈 이름 문자열의 집합입니다. 이 방식으로 모듈을 lazy로 만들면 해당 모듈을 사용하는 from-import도 lazy가 되지만, 하위 모듈의 import까지 반드시 lazy가 되는 것은 아닙니다.
일반적인(비-lazy) import 문은 전역 lazy imports 플래그를 확인합니다. 플래그가 “all”이면 모든 import가 potentially lazy 상태가 됩니다(위에서 언급한 것처럼 lazy가 될 수 없는 import는 제외합니다).
예:
__lazy_modules__ = ["json"]
import json
print('json' in sys.modules) # False
result = json.dumps({"hello": "world"})
print('json' in sys.modules) # True
전역 지연 임포트 플래그가 “none”으로 설정되면 잠재적으로 지연 가능한 임포트는 어떤 것도 지연 방식으로 임포트되지 않으며, 동작은 일반 임포트 문과 동일합니다. 즉, 해당 임포트는 즉시 수행됩니다(지연 키워드를 사용하지 않은 것처럼 동작합니다).
마지막으로 애플리케이션은 모든 potentially lazy가져오기에 사용자 지정 필터 함수를 적용하여 해당 가져오기를 지연 가져오기로 처리할지 여부를 결정할 수 있습니다(Lazy imports filter 참조). 이는 고급 기능입니다. 필터 함수가 설정되어 있으면 가져오기를 수행하는 모듈의 이름, 가져오는 모듈의 이름, 그리고 해당하는 경우 fromlist가 인자로 전달되어 호출됩니다. 필터 함수가 True를 반환하는 경우에만 가져오기가 지연 상태로 유지됩니다. 지연 가져오기 필터가 설정되지 않으면 모든 potentially lazy가져오기가 지연 가져오기가 됩니다.
참고 사항: .pth 파일
지연 가져오기 메커니즘은 site 모듈이 처리하는 .pth 파일에는 적용되지 않습니다. 관련 .pth 파일에서는 import 뒤에 공백이나 탭이 오는 것으로 시작하는 줄을 특별히 처리하지만, 이 특별 처리는 지연 임포트를 지원하도록 변경되지 않습니다. 관련 .pth 파일에 지정된 임포트는 언제나 그랬듯이 즉시 수행됩니다.
지연 객체
지연 모듈과 모듈에서 지연 가져오기된 이름은 types.LazyImportType 인스턴스로 표현되며, 사용하기 전에 실제 객체로 확인되어야 합니다(구체화). 이러한 구체화는 일반적으로 자동으로 수행되지만(아래 참조), 지연 객체의 resolve 메서드를 호출하여 수행할 수도 있습니다.
지연 가져오기 메커니즘
가져오기가 지연 방식이면 __import__대신 __lazy_import__이 호출됩니다. __lazy_import__은 __import__와 동일한 함수 시그니처를 가집니다. 이 함수는 모듈 이름을 sys.lazy_modules에 추가하고, 해당 모듈에 대해 완전히 수식된 모듈 이름을 포함하는 집합을 반환합니다. 이 모듈 이름은 어느 시점에 지연 가져오기되었는지를 기록하며, 주로 진단 및 자기성찰에 사용됩니다. 또한 모듈에 대한 types.LazyImportType 객체를 반환합니다.
from ... import의 구현(즉, IMPORT_FROM 바이트코드 구현)은 가져올 대상 모듈이 지연 모듈 객체인지 확인하며, 그렇다면 각 이름에 대해 대신 types.LazyImportType를 반환합니다.
이 과정의 최종 결과는 지연 가져오기가 활성화된 방식과 관계없이 지연 객체가 전역 변수에 할당된다는 것입니다.
지연 모듈 객체는 sys.modules에 나타나지 않고, sys.lazy_modules 집합에만 나열됩니다. 정상적인 작동에서는 지연 객체가 전역 변수에만 저장되어야 하며, 해당 변수에 접근하는 일반적인 방법(일반 변수 접근 및 모듈 속성)은 지연 가져오기를 구체화하고 접근 시 이를 대체합니다.
디버거와 같은 다른 수단을 통해 지연 객체를 노출하는 것도 여전히 가능합니다. 이는 문제로 간주되지 않습니다.
구체화
지연 객체를 사용하려면 이를 구체화해야 합니다. 이는 프로그램의 해당 시점에 가져오기를 확인하고 지연 객체를 구체 객체로 대체한다는 의미입니다. 구체화는 프로그램의 해당 시점에 모듈을 가져옵니다. 특히 구체화는 가져오기를 확인하기 위해 여전히 __import__를 호출하며, 이 호출은 reification 시점의 가져오기 시스템 상태(예: sys.path, sys.meta_path, sys.path_hooks 및 __import__)를 사용합니다. 이는 lazy import 문이 평가된 시점의 상태를 사용하는 것이 not 아닙니다.
모듈이 구체화되면 sys.lazy_modules에서 제거됩니다(해당 모듈에 대한 구체화되지 않은 다른 지연 참조가 여전히 있더라도 마찬가지입니다). 패키지가 구체화되고 해당 패키지의 서브모듈도 이전에 지연 가져오기된 경우, 해당 서브모듈은 자동으로 구체화되지 not 않지만 구체화된 패키지의 전역 변수에 추가are됩니다(패키지가 이미 서브모듈 이름에 다른 값을 할당한 경우는 제외합니다).
구체화에 실패하면(예: ImportError로 인해) 지연 객체는 구체화되거나 대체되지 not 않습니다. 이후 지연 객체를 사용하면 구체화를 다시 시도합니다. 구체화 중 발생한 예외는 정상적으로 발생하지만, 지연 가져오기가 정의된 위치와 접근된 위치를 모두 보여 주도록 예외 연결 정보가 추가됩니다(해당 예외가 구체화를 유발한 코드에서 전파되더라도 마찬가지입니다). 이를 통해 명확한 디버깅 정보를 제공합니다:
# app.py - has a typo in the import
lazy from json import dumsp # Typo: should be 'dumps'
print("App started successfully")
print("Processing data...")
# Error occurs here on first use
result = dumsp({"key": "value"})
트레이스백에는 두 위치가 모두 표시됩니다:
App started successfully
Processing data...
Traceback (most recent call last):
File "app.py", line 2, in <module>
lazy from json import dumsp
ImportError: lazy import of 'json.dumsp' raised an exception during resolution
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "app.py", line 8, in <module>
result = dumsp({"key": "value"})
^^^^^
ImportError: cannot import name 'dumsp' from 'json'. Did you mean: 'dump'?
이 예외 연쇄는 다음을 명확히 보여 줍니다:
- 지연 임포트가 정의된 위치,
- 모듈이 즉시 임포트되지 않았다는 사실, 그리고
- 오류를 발생시킨 실제 접근이 일어난 위치입니다.
이전에 지연 임포트된 모듈이 이후에 즉시 임포트되더라도 실체화는 자동으로 발생하지 않습니다. 실체화는 해당 모듈을 참조하는 모든 지연 객체(예: lazy from 문)를 즉시 해결하지 않습니다. 접근 중인 지연 객체만 해결합니다.
지연 객체(전역 변수 또는 모듈 속성을 통한)에 접근하면 해당 객체가 실체화됩니다.
그러나 globals()를 호출하거나 모듈의 __dict__에 접근해도 실체화가 트리거되지 않습니다 – 이러한 작업은 모듈의 딕셔너리를 반환하며, 해당 딕셔너리를 통해 지연 객체에 접근해도 사용할 때 수동으로 실체화해야 하는 지연 프록시 객체가 여전히 반환됩니다. resolve 메서드를 호출하여 지연 객체를 명시적으로 해결할 수 있습니다. 전역 범위에서 dir()를 호출해도 전역 객체가 실체화되지 않으며, mod.__dir__의 특수 처리로 인해 dir(mod)을 호출해도 마찬가지입니다. 전역에 임의로 접근하는 더 간접적인 방법(예: frame.f_globals 검사) 또한 모든 객체를 실체화하지 않습니다.
globals()와 __dict__를 사용하는 예:
# my_module.py
import sys
lazy import json
# Calling globals() does NOT trigger reification
g = globals()
print('json' in sys.modules) # False - still lazy
print(type(g['json'])) # <class 'LazyImport'>
# Accessing __dict__ also does NOT trigger reification
d = __dict__
print(type(d['json'])) # <class 'LazyImport'>
# Explicitly reify using the resolve() method
resolved = g['json'].resolve()
print(type(resolved)) # <class 'module'>
print('json' in sys.modules) # True - now loaded
참조 구현
참조 구현은 다음에서 사용할 수 있습니다: https://github.com/LazyImportsCabal/cpython/tree/lazy
평가 목적으로 데모를 사용할 수 있습니다(최신 PEP와 반드시 동기화되어 있지는 않음): https://lazy-import-demo.pages.dev/
바이트코드와 적응형 특수화
지연 임포트는 네 가지 바이트코드 명령인 IMPORT_NAME, IMPORT_FROM, LOAD_GLOBAL, LOAD_NAME를 수정하여 구현됩니다.
lazy 구문은 IMPORT_NAME명령의 oparg에 플래그(oparg & 0x01)를 설정합니다. 인터프리터는 이 플래그를 확인하고 _PyEval_ImportName()대신 _PyEval_LazyImportName()를 호출하여 임포트를 즉시 실행하는 대신 지연 임포트 객체를 생성합니다. IMPORT_FROM명령은 소스가 지연 임포트인지(PyLazyImport_CheckExact()인지) 확인하고 속성에 즉시 접근하는 대신 해당 속성에 대한 지연 객체를 생성합니다.
지연 객체에 접근하면 객체를 실체화해야 합니다. LOAD_GLOBAL명령(함수 범위에서 사용됨)과 LOAD_NAME명령(모듈 및 클래스 수준에서 사용됨)은 모두 로드 중인 객체가 지연 임포트인지 확인합니다. 그렇다면 실제 임포트를 수행하고 모듈을 sys.modules에 저장하기 위해 _PyImport_LoadLazyImportTstate()를 호출합니다.
이 확인으로 인해 각 접근에 아주 작은 비용이 발생합니다. 그러나 Python의 적응형 인터프리터는 지연 임포트가 실체화된 것을 관찰한 후 LOAD_GLOBAL을 특수화할 수 있습니다. 여러 번 실행하면 LOAD_GLOBAL은 지연 임포트를 확인하지 않고 모듈 딕셔너리에 직접 접근하는 LOAD_GLOBAL_MODULE이 됩니다.
생성된 바이트코드의 예:
lazy import json # IMPORT_NAME with flag set
다음을 생성합니다:
IMPORT_NAME 1 (json + lazy)
lazy from json import dumps # IMPORT_NAME + IMPORT_FROM
다음을 생성합니다:
IMPORT_NAME 1 (json + lazy)
IMPORT_FROM 1 (dumps)
lazy import json
x = json # Module-level access
다음을 생성합니다:
LOAD_NAME 0 (json)
lazy import json
def use_json():
return json.dumps({}) # Function scope
호출 전:
LOAD_GLOBAL 0 (json)
LOAD_ATTR 2 (dumps)
여러 번 호출한 후 LOAD_GLOBAL은 LOAD_GLOBAL_MODULE로 특수화됩니다:
LOAD_GLOBAL_MODULE 0 (json)
LOAD_ATTR_MODULE 2 (dumps)
지연 임포트 필터
Note
이는 고급 기능입니다. 이는 전역 플래그를 사용할 때 지연 임포트 동작을 세밀하게 제어해야 하는 전문 사용자 및 고급 사용자를 위한 것입니다. 이러한 함수는 애플리케이션의 런타임 실행에 영향을 줄 수 있으므로 라이브러리 개발자는 사용하지 않는 것이 좋습니다(sys.setrecursionlimit(), sys.setswitchinterval() 또는 gc.set_threshold()와 유사합니다).
이 PEP는 지연 임포트 필터를 관리하기 위해 sys모듈에 다음과 같은 새 함수를 추가합니다.
sys.set_lazy_imports_filter(func)- 필터 함수를 설정합니다.func=None이면 임포트 필터가 제거됩니다.func매개변수는 다음 시그니처를 가져야 합니다:func(importer: str, name: str, fromlist: tuple[str, ...] | None) -> boolsys.get_lazy_imports_filter()- 현재 설치된 필터 함수를 반환하거나, 설정된 필터가 없으면None을 반환합니다.sys.set_lazy_imports(mode, /)- 런타임에 지연 임포트를 제어하기 위한 프로그래밍 방식 API입니다.mode매개변수는"normal"(lazy키워드만 따름),"all"(모든 임포트를 잠재적으로 지연 임포트로 강제함) 또는"none"(모든 임포트를 즉시 임포트하도록 강제함)일 수 있습니다.sys.get_lazy_imports()- 현재 지연 임포트 모드를 문자열로 반환합니다:"normal","all"또는"none".
필터 함수는 잠재적으로 지연될 수 있는 모든 임포트에 대해 호출되며, 해당 임포트가 지연되어야 하면 True를 반환해야 합니다. 이를 통해 어떤 임포트를 지연해야 하는지 세밀하게 제어할 수 있으며, 알려진 부작용 의존성이나 등록 패턴이 있는 모듈을 제외하는 데 유용합니다. 필터 함수는 재구체화 시점이 아니라 지연 임포트 또는 지연 from 임포트 문이 실행되는 시점에 호출됩니다. 필터 함수는 동시에 호출될 수 있습니다.
필터 메커니즘은 도구, 디버거, 린터 및 기타 생태계 유틸리티가 더 나은 지연 임포트 경험을 제공하는 데 활용할 수 있는 기반 역할을 합니다. 예를 들어 정적 분석 도구는 부작용이 있는 모듈을 감지하고 적절한 필터를 자동으로 구성할 수 있습니다. 향후에는 (이 PEP의 범위 밖) 이 기반을 통해 패키지 메타데이터, 지연 임포트 안전성 어노테이션이 포함된 타입 스텁 또는 구성 파일과 같이 어떤 모듈이 지연 임포트에 안전한지 선언적으로 지정하는 더 나은 방법을 사용할 수 있게 될 수 있습니다. 현재 필터 API는 핵심 언어 사양을 변경하지 않고도 이러한 향후 개선 사항을 수용할 수 있을 만큼 유연하게 설계되었습니다.
예:
import sys
def exclude_side_effect_modules(importer, name, fromlist):
"""
Filter function to exclude modules with import-time side effects.
Args:
importer: Name of the module doing the import
name: Name of the module being imported
fromlist: Tuple of names being imported (for 'from' imports), or None
Returns:
True to allow lazy import, False to force eager import
"""
# Modules known to have important import-time side effects
side_effect_modules = {'legacy_plugin_system', 'metrics_collector'}
if name in side_effect_modules:
return False # Force eager import
return True # Allow lazy import
# Install the filter
sys.set_lazy_imports_filter(exclude_side_effect_modules)
# These imports are checked by the filter
lazy import data_processor # Filter returns True -> stays lazy
lazy import legacy_plugin_system # Filter returns False -> imported eagerly
print('data_processor' in sys.modules) # False - still lazy
print('legacy_plugin_system' in sys.modules) # True - loaded eagerly
# First use of data_processor triggers loading
result = data_processor.transform(data)
print('data_processor' in sys.modules) # True - now loaded
전역 지연 임포트 제어
참고: 이것은 고급 기능입니다. 이는 애플리케이션 전체에서 지연 임포트를 제어해야 하는 애플리케이션 개발자 및 프레임워크 작성자를 위한 것입니다. 라이브러리 개발자는 전역 활성화 메커니즘을 사용하지 않는 것이 좋습니다. 이는 애플리케이션의 런타임 실행에 영향을 줄 수 있기 때문입니다(``sys.setrecursionlimit()``, ``sys.setswitchinterval()``, ``gc.set_threshold()``와 유사합니다).
전역 지연 임포트 플래그는 다음을 통해 제어할 수 있습니다.
-X lazy_imports=<mode>명령줄 옵션PYTHON_LAZY_IMPORTS=<mode>환경 변수sys.set_lazy_imports(mode)함수(주로 테스트용)
지연 임포트 모드를 설정할 때의 우선순위는 표준 Python 패턴을 따릅니다. sys.set_lazy_imports()가 가장 높은 우선순위를 가지며, 그다음은 -X lazy_imports=<mode>, 이어서 PYTHON_LAZY_IMPORTS=<mode>순입니다. 아무것도 지정하지 않으면 모드는 "normal"로 기본 설정됩니다.
여기서 <mode>는 다음과 같을 수 있습니다.
"normal"(또는 설정하지 않음): 명시적으로 지연으로 표시된 임포트만 지연됩니다."all":try블록과import *를 제외한 모듈 수준의 모든 가져오기가 잠재적으로 지연된 상태가 됩니다."none":lazy키워드로 명시적으로 표시된 가져오기조차 어떤 가져오기도 지연되지 않습니다.
전역 플래그가 "all"로 설정되면, try 블록 내부에 있거나 와일드카드(from ... import *) 임포트인 경우를 제외하고, 모든 모듈의 전역 수준에 있는 모든 임포트가 잠재적으로 지연 가능해집니다.
전역 지연 임포트 플래그가 "none"으로 설정되면 잠재적으로 지연 가능한 임포트는 어떤 것도 지연 방식으로 임포트되지 않고, 임포트 필터도 호출되지 않으며, 동작은 일반 import 문과 동일합니다. 즉, 해당 임포트는 즉시 수행됩니다(지연 키워드를 사용하지 않은 것처럼 동작합니다).
파이썬 코드는 환경 또는 CLI에서 상속된 전역 지연 가져오기 플래그의 상태를 재정의하기 위해 sys.set_lazy_imports()함수를 실행할 수 있습니다. 애플리케이션에서 sys.set_lazy_imports("none")을 통해 모든 가져오기가 즉시 평가되도록 해야 하는 경우에 특히 유용합니다.
하위 호환성
지연 가져오기는 옵트인입니다. 프로젝트에서 명시적으로 지연 동작을 활성화하지 않는 한(lazy 구문, __lazy_modules__, 또는 인터프리터 전체 스위치를 통해) 기존 프로그램은 변경 없이 계속 실행됩니다.
변경되지 않는 의미론
- 일반
import및from ... import ...문은 제공된 로컬 또는 전역 메커니즘에 의해 명시적으로 잠재적으로 지연된 상태가 되지 않는 한 계속 즉시 실행됩니다. - 동적 가져오기 API는 계속 즉시 실행되며 변경되지 않습니다:
__import__()및importlib.import_module()입니다. - 지연 객체가 재구체화될 때 가져오기 훅과 로더는 표준 가져오기 프로토콜에 따라 계속 실행됩니다.
관찰 가능한 동작 변화 (옵트인만 해당)
이러한 변경은 명시적으로 지연된 상태로 만든 바인딩으로 제한됩니다.
- 오류 시점. 즉시 가져오기 중에 발생했을 예외(예를 들어 누락된 멤버에 대한
ImportError또는AttributeError)가 이제 지연된 이름을 사용할 때 발생합니다.# With eager import - error at import statement import broken_module # ImportError raised here # With lazy import - error deferred lazy import broken_module print("Import succeeded") broken_module.foo() # ImportError raised here on use
- 부작용 시점. 지연 방식으로 가져온 모듈의 가져오기 시 부작용은 모듈 가져오기 시점이 아니라 바인딩을 처음 사용할 때 발생합니다.
- 가져오기 순서. 모듈은 처음 사용할 때 가져오기되므로, 모듈을 가져오는 순서가 코드에 나타나는 순서와 다를 수 있습니다.
- ``sys.modules``에서의 존재 여부. 지연 방식으로 가져온 모듈은 처음 사용할 때까지
sys.modules에 나타나지 않습니다. 재구체화된 후에는sys.modules에 나타나야 합니다. 다른 코드가 처음 사용하기 전에 동일한 모듈을 즉시 가져오는 경우, 지연 바인딩은 처음 사용될 때 해당 기존의 (지연된) 모듈 객체로 확인됩니다. - 프록시 가시성. 처음 사용하기 전에는 바인딩된 이름이 지연 프록시를 가리킵니다. 값에 접근하는 간접 인트로스펙션에서는 프록시인 지연 객체 표현을 관찰할 수 있습니다. 처음 사용한 후(모듈을 성공적으로 가져온 경우) 이름은 실제 객체에 다시 바인딩되며 즉시 가져오기와 구별할 수 없게 됩니다.
스레드 안전성과 재구체화
재구체화는 기존 가져오기 잠금 규율을 따릅니다. 정확히 하나의 스레드가 가져오기를 수행하고, 가져오는 모듈의 전역을 확인된 객체에 원자적으로 재바인딩합니다. 이후 동시에 읽는 스레드들은 실제 객체를 관찰합니다.
지연 가져오기는 스레드 안전하며 프리 스레딩에 대한 특별한 고려 사항이 없습니다. 일반적으로 주 스레드에서 가져오는 모듈도 해당 스레드가 지연 가져오기에 처음 접근하도록 트리거하면 다른 스레드에서 가져올 수 있습니다. 이는 문제가 되지 않습니다. 가져오기 잠금이 가져오기를 수행하는 스레드와 관계없이 스레드 안전성을 보장하기 때문입니다.
서브인터프리터가 지원됩니다. 각 서브인터프리터는 자체 sys.lazy_modules 및 임포트 상태를 유지하므로, 한 서브인터프리터의 지연 임포트가 다른 서브인터프리터에 영향을 주지 않습니다.
성능
지연 임포트에는 측정 가능한 성능 오버헤드가 없습니다. 이 구현은 지연 임포트를 사용하는 코드와 사용하지 않는 코드 모두에서 성능 중립적이도록 설계되었습니다.
런타임 성능
실체화 후(임포트가 성공한 경우), 지연 임포트에는 오버헤드가 전혀 없습니다. 적응형 인터프리터는 바이트코드를 특수화하여(일반적으로 2~3회 액세스 후) 모든 검사를 제거합니다. 예를 들어, LOAD_GLOBAL은 LOAD_GLOBAL_MODULE이 되며, 이는 일반 임포트와 동일한 방식으로 모듈에 직접 액세스합니다.
pyperformance suite는 이 구현이 성능 중립적임을 확인합니다.
필터 함수 성능
(sys.set_lazy_imports_filter()로 설정된) 필터 함수는 각 잠재적으로 지연될 수 있는 임포트에 대해 호출되어 해당 임포트를 실제로 지연할지 결정합니다. 필터가 설정되지 않은 경우 이는 단순한 NULL 검사(필터 함수가 등록되었는지 확인하는 검사)이며, 매우 예측 가능한 분기이므로 사실상 오버헤드를 추가하지 않습니다. 필터가 설치된 경우 각 잠재적으로 지연될 수 있는 임포트마다 호출되지만, 이 경우에도 측정 가능한 성능 비용이 거의 없습니다. 이를 측정하기 위해 Python 표준 라이브러리의 최상위 임포트 가능 모듈 278개를 모두 임포트하는 벤치마크를 수행했습니다. 이 과정에서 모든 하위 모듈과 종속성을 포함하여 총 392개의 모듈이 전이적으로 로드되었으며, 이후 로드된 모든 모듈을 강제로 실체화하여 모든 항목이 완전히 구체화되도록 했습니다.
이러한 측정값은 필터 메커니즘 자체의 기준 오버헤드를 설정한다는 점에 유의하십시오. 물론 사소한 검사 이상의 추가 작업을 수행하는 사용자 정의 필터 함수는 해당 작업의 복잡성에 비례하는 오버헤드를 추가합니다. 그러나 실제로는 불필요한 임포트를 방지하여 얻는 성능상의 이점에 비해 이러한 오버헤드가 훨씬 작을 것으로 예상합니다. 아래 벤치마크는 필터 함수가 사실상 아무 작업도 수행하지 않을 때 필터 디스패치 메커니즘에 드는 최소 비용을 측정합니다.
저희는 서로 다른 네 가지 구성을 비교했습니다.
| 구성 | 평균 ± 표준편차 (ms) | 기준선 대비 오버헤드 |
|---|---|---|
| 즉시 임포트(기준선) | 161.2 ± 4.3 | 0% |
| 지연 + 즉시 실행을 강제하는 필터 | 161.7 ± 4.2 | +0.3% ± 3.7% |
| 지연 + 지연을 허용하는 필터 + 구체화 | 162.0 ± 4.0 | +0.5% ± 3.7% |
| 지연 + 필터 없음 + 구체화 | 161.4 ± 4.3 | +0.1% ± 3.8% |
네 가지 구성은 다음과 같습니다.
- 즉시 임포트(기준선): 지연 임포트 기능이 없는 일반적인 Python 임포트입니다. 표준 Python 동작입니다.
- 지연 임포트 + 즉시 실행을 강제하는 필터: 필터 함수가 모든 임포트에 대해
False를 반환하여 즉시 실행을 강제한 다음, 스크립트 종료 시점에 모든 임포트를 실체화합니다. 모든 임포트가 필터를 거치지만 즉시 실행되므로 순수한 필터 호출 오버헤드를 측정합니다. - 지연 임포트 + 지연을 허용하는 필터 + 실체화: 필터 함수가 모든 임포트에 대해
True를 반환하여 지연 실행을 허용합니다. 스크립트 종료 시점에 모든 임포트를 실체화합니다. 임포트가 실제로 지연될 때의 필터 오버헤드를 측정합니다. - 지연 임포트 + 필터 없음 + 실체화: 필터를 설치하지 않고 임포트를 지연한 다음 스크립트 종료 시점에 실체화합니다. 필터가 없는 지연 동작의 기준선입니다.
벤치마크에서는 hyperfine를 사용하여 표준 라이브러리 모듈 278개를 테스트합니다. 각각은 새로운 Python 프로세스에서 실행합니다. 공정한 비교를 위해 모든 구성에서 정확히 동일한 모듈 집합(즉시 가져오기 기준선에서 로드하는 모든 모듈)을 가져오도록 강제합니다.
벤치마크 환경에서는 32개의 논리 CPU(0-15는 3200MHz, 16-31은 2400MHz)를 CPU 격리로 사용하고, 성능 스케일링 거버너를 적용했으며, Turbo Boost를 비활성화하고 전체 ASLR 무작위화를 사용합니다. 오버헤드 오차 막대는 (value - baseline) / baseline 공식에 표준 오차 전파를 적용하여 계산하며, 측정값과 기준선 모두의 불확실성을 고려합니다.
시작 시간 개선
지연 가져오기의 주요 성능 이점은 시작 시 전체 의존성 트리를 낙관적으로 로드하는 대신 런타임에 실제로 사용되는 모듈만 로드하여 시작 시간을 줄이는 것입니다.
실제 환경에서 대규모로 배포한 결과 이러한 이점이 막대할 수 있음이 입증되었지만, 물론 구체적인 코드베이스와 사용 패턴에 따라 달라집니다. 규모가 크고 서로 긴밀히 연결된 코드베이스를 보유한 조직에서는 서버 다시 로드 시간, ML 학습 초기화, 명령줄 도구 시작 시간, Jupyter 노트북 로드 시간이 상당히 감소했다고 보고했습니다. 사용되지 않는 모듈이 로드되지 않은 상태로 유지되면서 메모리 사용량도 개선되는 것으로 관찰되었습니다.
실제 운영 환경 배포의 자세한 사례 연구와 성능 데이터는 다음을 참조하십시오:
- Python Lazy Imports With Cinder (Meta Instagram Server)
- Lazy is the new fast: How Lazy Imports and Cinder accelerate machine learning at Meta (Meta ML Workloads)
- Inside HRT’s Python Fork (Hudson River Trading)
- Create an On-Demand Initializer for PySide (Qt for Python/PySide) - PEP 690의 아이디어를 기반으로 한 Christian Tismer의 PySide6 지연 초기화 구현으로, PySide 애플리케이션의 시작 시간이 10~20% 개선됨을 보여 줍니다. 이는 가져오기 시 광범위한 초기화를 수행하는 프레임워크에서 지연 가져오기가 특히 가치 있음을 보여 줍니다.
이점은 코드베이스의 복잡도에 따라 확장됩니다. 코드베이스가 클수록, 그리고 서로 더 긴밀히 연결되어 있을수록 개선 효과는 더욱 커집니다. PySide 구현은 초기화 오버헤드가 큰 프레임워크가 선택적 지연 로딩을 통해 상당한 이점을 얻을 수 있음을 특히 잘 보여 줍니다.
타입 지정 및 도구
타입 검사기와 정적 분석기는 이름 해석을 위해 lazy 가져오기를 일반 가져오기처럼 처리할 수 있습니다. 런타임에는 시작 오버헤드를 피하기 위해 어노테이션 전용 가져오기를 lazy로 표시할 수 있습니다. IDE와 디버거는 최초 사용 전에는 지연 프록시를 표시하고, 그 이후에는 실제 객체를 표시할 수 있도록 준비해야 합니다.
보안 관련 사항
동일한 환경에서 가져오기를 수행하면서 패키지를 설치하는 도구는 설치 단계 전에 모든 모듈을 즉시 가져오거나 실체화해야 새로 설치된 배포 패키지가 해당 모듈을 가리지 않도록 할 수 있습니다.
이러한 도구는 sys.set_lazy_imports()를 "none"으로 설정하여 즉시 평가를 강제하거나, 세밀하게 제어할 수 있도록 sys.set_lazy_imports_filter() 함수를 제공할 수 있습니다.
이 내용을 가르치는 방법
새로운 lazy 키워드는 언어 표준의 일부로 문서화됩니다.
이 기능은 선택적으로 활성화되므로, 새로운 Python 사용자는 익숙한 방식대로 계속 언어를 사용할 수 있어야 합니다. 숙련된 개발자는 위에 나열한 다양한 이점(지연 시간 감소, 메모리 사용량 감소 등)을 사례별로 활용하기 위해 지연 가져오기를 사용할 것으로 예상합니다. Python 바이너리의 성능에 관심이 있는 개발자는 프로파일링을 활용하여 코드베이스에서 가져오기 시간 오버헤드를 파악하고 필요한 가져오기를 lazy로 표시할 가능성이 높습니다. 또한 개발자는 타입 어노테이션에만 사용되는 가져오기를 lazy로 표시할 수 있습니다.
Python 문서에 추가 문서가 포함되며, 여기에는 지침, 전용 방법 안내서, 다음 내용을 다루는 가져오기 시스템 문서 업데이트가 포함됩니다: (-X importtime과 같은) 프로파일링 도구를 사용한 느리게 로드되는 모듈 식별, 기존 코드베이스의 마이그레이션 전략, 가져오기 시간 부작용으로 인한 일반적인 함정을 피하기 위한 모범 사례, 타입 어노테이션 및 순환 가져오기와 함께 지연 가져오기를 효과적으로 사용하는 패턴.
아래는 지연 임포트를 최대한 활용하고 비호환성을 피하는 방법에 대한 지침입니다.
- 지연 임포트를 도입할 때 사용자는 임포트를 사용할 때까지 생략하면 부작용이 실행되지 않는다는 점을 알아야 합니다. 따라서 사용자는 임포트 시점의 부작용에 의존하는 모듈을 주의해야 합니다. 임포트 부작용에 의존하는 가장 일반적인 경우는 레지스트리 패턴일 것입니다. 이 패턴에서는 일부 외부 레지스트리의 항목이 모듈을 임포트하는 동안 암묵적으로 채워지며, 대개 데코레이터를 사용하지만 때로는 메타클래스 또는
__init_subclass__를 통해 구현됩니다. 대신 객체 레지스트리는 명시적인 검색 프로세스(예: 호출할 잘 알려진 함수)를 통해 구성해야 합니다.# Problematic: Plugin registers itself on import # my_plugin.py from plugin_registry import register_plugin @register_plugin("MyPlugin") class MyPlugin: pass # In main code: lazy import my_plugin # Plugin NOT registered yet - module not loaded! # Better: Explicit discovery # plugin_registry.py def discover_plugins(): from my_plugin import MyPlugin register_plugin(MyPlugin) # In main code: plugin_registry.discover_plugins() # Explicit loading
- 필요한 서브모듈은 항상 명시적으로 임포트하십시오. 다른 임포트에 의존하여 모듈이 서브모듈을 속성으로 갖도록 보장하는 것만으로는 충분하지 않습니다. 명시적으로
foo/__init__.py에from . import bar가 있지 않은 한, 항상import foo.bar; foo.bar.Baz를 사용하고import foo; foo.bar.Baz는 사용하지 마십시오. 후자는 다른 곳에서foo.bar를 임포트한 부작용으로 속성foo.bar가 추가되기 때문에 (신뢰할 수 없지만) 작동할 뿐입니다. - 시작 시간을 개선하기 위해 임포트를 함수 안으로 옮기는 사용자는 대신 임포트를 원래 위치에 그대로 두고
lazy키워드를 추가하는 방법을 고려해야 합니다. 이렇게 하면 의존 관계를 명확하게 유지하고 임포트를 반복적으로 다시 확인하는 오버헤드를 피하면서도 프로그램의 실행 속도를 높일 수 있습니다.# Before: Inline import (repeated overhead) def process_data(data): import json # Re-resolved on every call return json.dumps(data) # After: Lazy import at module level lazy import json def process_data(data): return json.dumps(data) # Loaded once on first call
- 와일드카드(별표) 임포트는 항상 즉시 실행되므로 사용하지 마십시오.
자주 묻는 질문
거부된 PEP 690과 어떻게 다릅니까?
PEP 810은 PEP 690의 암묵적 전역 방식 대신 명시적인 옵트인 방식을 취합니다. 주요 차이점은 다음과 같습니다.
- 명시적 구문:
lazy import foo는 어떤 임포트가 지연되는지 명확하게 표시합니다. - 지역 범위: 지연성은 특정 임포트 문에만 영향을 미치며 의존 관계로 연쇄적으로 적용되지 않습니다.
- 더 단순한 구현: 핵심 딕셔너리 동작을 수정하는 대신 프록시 객체를 사용합니다.
실체화 시점에 무엇이 변경됩니까? 무엇이 동일하게 유지됩니까?
변경되는 사항(시점):
- 언제 모듈이 임포트되는가 - 임포트 문 시점이 아니라 처음 사용될 때까지 지연됩니다.
- 언제 임포트 오류가 발생하는가 - 임포트 시점이 아니라 처음 사용될 때 발생합니다.
- 언제 모듈 수준 부작용이 실행되는가 - 임포트 시점이 아니라 처음 사용될 때 실행됩니다.
동일하게 유지되는 사항(그 밖의 모든 것):
- 사용되는 임포트 메커니즘 - 동일한
__import__, 동일한 훅, 동일한 로더를 사용합니다. - 생성되는 모듈 객체 - 즉시 임포트된 모듈과 동일합니다.
- 참조되는 임포트 상태 - 임포트 문 시점이 아니라 실체화 시점에
sys.path,sys.meta_path등을 참조합니다. - 모듈 속성과 동작 - 실체화된 후에는 완전히 구별할 수 없습니다.
- 스레드 안전성 - 일반 임포트와 동일한 임포트 잠금 규율을 따릅니다.
즉, 지연 임포트는 무언가가 발생하는 시점만 변경할 뿐, 무엇이 발생하는지는 변경하지 않습니다. 실체화된 후에는 지연 임포트된 모듈과 즉시 임포트된 모듈을 구별할 수 없습니다.
지연 임포트에서 오류가 발생하면 어떻게 됩니까?
임포트 오류(ImportError, ModuleNotFoundError, 구문 오류)는 지연 이름을 처음 사용할 때까지 지연됩니다. 이는 임포트를 함수 내부로 옮기는 것과 비슷합니다. 지연 객체에 처음 접근한 위치를 가리키는 명확한 트레이스백과 함께 오류가 발생합니다.
구현에서는 예외 체이닝을 통해 향상된 오류 보고를 제공합니다. 실체화 중 지연 임포트가 실패하면 원래 예외가 보존되고 체이닝되며, 임포트가 정의된 위치와 처음 사용된 위치가 모두 표시됩니다:
Traceback (most recent call last):
File "test.py", line 1, in <module>
lazy import broken_module
ImportError: lazy import of 'broken_module' raised an exception during resolution
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "test.py", line 3, in <module>
broken_module.foo()
^^^^^^^^^^^^^
File "broken_module.py", line 2, in <module>
1/0
ZeroDivisionError: division by zero
실체화 중 발생한 예외는 지연 객체의 교체를 방지하며, 이후 지연 객체를 사용하면 전체 실체화를 다시 시도합니다.
지연 임포트는 임포트 시점의 부작용이 있는 모듈에 어떤 영향을 줍니까?
부작용은 처음 사용할 때까지 지연됩니다. 이는 일반적으로 성능 측면에서 바람직하지만, 임포트 시점의 등록 패턴에 의존하는 모듈에는 코드 변경이 필요할 수 있습니다. 다음을 권장합니다:
- 임포트 시점의 부작용 대신 명시적 초기화 함수를 사용하십시오
- 필요할 때 초기화 함수를 명시적으로 호출하십시오
- 부작용을 위해 임포트 순서에 의존하지 마십시오
from ... import ... 문과 함께 지연 임포트를 사용할 수 있습니까?
예, from ... import *를 사용하지 않는 한 그렇습니다. lazy import foo와 lazy from foo import bar를 모두 지원합니다. bar 이름은 처음 사용할 때 foo.bar로 해석되는 지연 객체에 바인딩됩니다.
lazy from module import Class는 전체 모듈을 로드합니까, 아니면 클래스만 로드합니까?
전체 모듈을 로드하며, 클래스만 로드하지는 않습니다. 이는 Python의 임포트 시스템이 항상 모듈 파일 전체를 실행하기 때문입니다. .py 파일의 일부만 실행하는 메커니즘은 없습니다. Class에 처음 접근하면 Python은 다음을 수행합니다:
- 전체
module.py파일을 로드하고 실행합니다 - 결과 모듈 객체에서
Class특성을 추출합니다 Class를 네임스페이스의 이름에 바인딩합니다
이는 즉시 임포트하는 from module import Class 동작과 동일합니다. 지연 임포트와의 유일한 차이점은 1~3단계가 임포트 문에서가 아니라 처음 사용할 때 수행된다는 것입니다.
# heavy_module.py
print("Loading heavy_module") # This ALWAYS runs when module loads
class MyClass:
pass
class UnusedClass:
pass # Also gets defined, even though we don't import it
# app.py
lazy from heavy_module import MyClass
print("Import statement done") # heavy_module not loaded yet
obj = MyClass() # NOW "Loading heavy_module" prints
# (and UnusedClass gets defined too)
핵심 사항: 지연 임포트는 모듈이 로드되는 시점을 미룰 뿐, 무엇이 로드되는지는 미루지 않습니다. 모듈의 일부만 선택적으로 로드할 수는 없습니다 – Python의 임포트 시스템은 부분적인 모듈 실행을 지원하지 않습니다.
타입 어노테이션과 TYPE_CHECKING 임포트는 어떻게 됩니까?
지연 임포트는 TYPE_CHECKING 가드가 필요한 일반적인 경우를 없앱니다. 다음과 같이 작성할 수 있습니다:
lazy from collections.abc import Sequence, Mapping # No runtime cost
def process(items: Sequence[str]) -> Mapping[str, int]:
...
다음 대신:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Sequence, Mapping
def process(items: Sequence[str]) -> Mapping[str, int]:
...
지연 임포트의 성능 오버헤드는 어느 정도입니까?
오버헤드는 최소화되어 있습니다:
- 적응형 인터프리터가 느린 경로를 최적화하여 제거하므로, 최초 사용 후에는 오버헤드가 없습니다(임포트가 실패하지 않는 경우).
- 프록시 객체를 생성하는 데 한 번만 발생하는 작은 비용이 있습니다.
- 실체화(최초 사용) 비용은 일반 임포트와 동일합니다.
- 지속적인 성능 저하는 없습니다.
pyperformance suite를 사용한 벤치마킹 결과, 지연 임포트를 사용하지 않을 때 구현의 성능이 중립적임을 보여 줍니다.
동일한 모듈을 지연 임포트와 즉시 임포트로 혼합하여 사용할 수 있습니까?
예, 가능합니다. 동일한 프로그램에서 모듈 foo를 지연 방식과 즉시 방식으로 모두 임포트하면, 즉시 임포트가 우선하며 두 바인딩 모두 동일한 모듈 객체로 해석됩니다.
기존 코드를 지연 임포트를 사용하도록 어떻게 마이그레이션합니까?
마이그레이션은 점진적으로 진행됩니다:
- 프로파일링 도구를 사용하여 느리게 로드되는 모듈을 식별하십시오.
- 즉시 필요하지 않은 임포트에
lazy키워드를 추가하십시오. - 부작용 발생 시점의 변경이 기능을 손상하지 않는지 테스트하십시오.
- 이전 Python 버전과의 호환성을 위해
__lazy_modules__를 사용하십시오.
스타 임포트(from module import *)는 어떻습니까?
와일드카드(스타) 임포트는 지연 방식으로 사용할 수 없으며, 계속 즉시 수행됩니다. 모듈을 로드하지 않고는 임포트되는 이름의 집합을 확인할 수 없기 때문입니다. 스타 임포트와 함께 lazy 키워드를 사용하면 구문 오류가 발생합니다. 지연 임포트가 전역적으로 활성화된 경우에도 스타 임포트는 즉시 수행됩니다.
지연 임포트는 임포트 훅 및 사용자 지정 로더와 어떻게 상호 작용합니까?
임포트 훅과 로더는 정상적으로 작동합니다. 지연 객체가 사용되면 실체화 시점에 적용되어 있던 사용자 지정 훅이나 로더를 포함하여 표준 임포트 프로토콜이 실행됩니다.
다중 스레드 환경에서는 어떻게 됩니까?
지연 임포트의 실체화는 스레드 안전합니다. 하나의 스레드만 실제 임포트를 수행하며, 바인딩은 원자적으로 업데이트됩니다. 다른 스레드에서는 지연 프록시 또는 최종적으로 확인된 객체 중 하나를 보게 됩니다.
사용하지 않고도 지연 임포트의 실체화를 강제할 수 있습니까?
예, 개별 지연 객체는 해당 resolve() 메서드를 호출하여 확인할 수 있습니다.
대신 importlib.util.LazyLoader를 사용하지 않는 이유는 무엇입니까?
표준 라이브러리의 LazyLoader는 특정 사용 사례를 위해 설계되었지만, 범용 지연 임포트 메커니즘으로서는 근본적인 한계가 있습니다.
가장 중요한 점은 LazyLoader가 from ... import 문을 지원하지 않는다는 것입니다. 모듈에서 특정 속성을 지연 임포트하는 간단한 메커니즘은 없습니다. 사용자는 개별 속성을 수동으로 감싸고 프록시해야 하며, 이는 오류가 발생하기 쉬울 뿐 아니라 성능상의 이점도 무효화합니다.
또한 LazyLoader는 지연 로더를 생성하기 전에 모듈 명세를 확인해야 하므로, 지연 로딩의 성능상 이점을 감소시키는 오버헤드가 발생합니다. 명세 확인에는 파일 시스템 작업과 경로 검색이 포함되지만, 이 PEP의 방식에서는 실제 모듈 사용 시점까지 이를 지연합니다.
LazyLoader는 언어 수준의 구문을 제공하는 대신 임포트 메커니즘 수준에서 동작하므로, 린터 및 타입 검사기와 같은 도구가 지연 임포트를 인식할 정식 방법이 없습니다. 전용 구문을 사용하면 생태계 전반의 표준화가 가능하고, 순수한 라이브러리 기반 방식으로는 불가능한 컴파일러 및 런타임 최적화도 수행할 수 있습니다.
마지막으로 LazyLoader는 모듈 명세, 로더 및 sys.modules를 수동으로 조작하는 상당한 보일러플레이트가 필요하므로, 여러 모듈을 지연 임포트해야 하는 일반적인 사용 사례에는 실용적이지 않습니다.
isort나 black같은 도구가 중단됩니까?
린터, 포매터 및 기타 도구는 lazy키워드를 인식하도록 업데이트해야 하지만, 임포트 구조는 그대로 유지되므로 변경 사항은 최소한이어야 합니다. 키워드가 맨 앞에 나타나므로 쉽게 구문 분석할 수 있습니다.
라이브러리가 지연 임포트와 호환되는지 어떻게 알 수 있습니까?
대부분의 라이브러리는 지연 임포트와 함께 정상적으로 작동할 것입니다. 문제가 발생할 수 있는 라이브러리는 다음과 같습니다.
- 임포트 시점에 필수적인 부작용(등록, 몽키 패칭)이 있는 라이브러리입니다.
- 특정 임포트 순서를 예상하는 라이브러리입니다.
- 임포트 중에 전역 상태를 변경하는 라이브러리입니다.
확실하지 않다면 특정 사용 사례에서 지연 임포트를 테스트하십시오.
지연 임포트 모드를 전역적으로 활성화했는데 라이브러리가 올바르게 작동하지 않으면 어떻게 됩니까?
참고: 고급 기능입니다. 문제가 있는 부작용이 발생하는 것으로 알려진 특정 모듈을 지연 임포트 필터를 사용하여 제외할 수 있습니다.
import sys
def my_filter(importer, name, fromlist):
# Don't lazily import modules known to have side effects
if name in {'problematic_module', 'another_module'}:
return False # Import eagerly
return True # Allow lazy import
sys.set_lazy_imports_filter(my_filter)
필터 함수는 임포터 모듈 이름, 임포트되는 모듈 및 fromlist( from ... import를 사용하는 경우)을 받습니다. False를 반환하면 즉시 임포트가 강제됩니다.
또는 디버깅을 위해 -X lazy_imports=none을 통해 전역 모드를 "none"으로 설정하여 모든 지연 임포트를 끌 수 있습니다.
함수 내부에서 지연 임포트를 사용할 수 있습니까?
아니요. lazy키워드는 모듈 수준에서만 허용됩니다. 함수 수준의 지연 로딩에는 기존의 인라인 임포트를 사용하거나, lazy를 사용하여 임포트를 모듈 수준으로 옮기십시오.
이전 Python 버전과의 향후 호환성은 어떻게 합니까?
호환성을 위해 __lazy_modules__ 전역 변수를 사용하십시오.
# Works on Python 3.15+ as lazy, eager on older versions
__lazy_modules__ = ['expensive_module', 'expensive_module_2']
import expensive_module
from expensive_module_2 import MyClass
모듈 이름 문자열의 목록인 __lazy_modules__ 속성이 있습니다. import 문이 실행되면 Python은 가져오는 모듈 이름이 __lazy_modules__에 나타나는지 확인합니다. 나타나면 해당 import는 lazy 키워드가 있는 것처럼 처리됩니다(잠재적으로 지연됨). 지연 임포트를 지원하지 않는 Python 3.15 이전 버전에서는 __lazy_modules__ 속성이 단순히 무시되고 import는 평소처럼 즉시 수행됩니다.
이는 lazy 키워드에 의존할 수 있을 때까지의 마이그레이션 경로를 제공합니다. 예측 가능성을 최대화하려면 모든 import보다 먼저 __lazy_modules__를 한 번 정의하는 것이 좋습니다. 하지만 각 import마다 확인되므로 import 문 사이에서 수정할 수 있습니다.
명시적 지연 임포트는 PEP 649 및 PEP 749와 어떻게 상호 작용합니까?
Python 3.14는 PEP 649 및 PEP 749에 명시된 어노테이션의 평가 지연을 구현했습니다. 어노테이션이 문자열화되지 않은 경우 나중에 평가되는 표현식입니다. 어노테이션에 접근할 때만 해석됩니다. 아래 예제에서는 사용자가 __annotations__딕셔너리를 검사할 때만 fake_typing 모듈이 로드됩니다. 사용자가 annotationlib.get_annotations() 또는 getattr을 사용하여 어노테이션에 접근하는 경우에도 fake_typing 모듈이 로드됩니다.
lazy from fake_typing import MyFakeType
def foo(x: MyFakeType):
pass
print(foo.__annotations__) # Triggers loading the fake_typing module
지연 임포트는 dir(), getattr(), 모듈 인트로스펙션과 어떻게 상호 작용합니까?
일반적인 속성 접근이나 getattr()를 통한 지연 임포트 접근은 접근한 속성의 실체화를 트리거합니다. 모듈에서 dir()을 호출하면 실체화를 방지하기 위해 mod.__dir__에서 특별히 처리됩니다.
lazy import json
# Before any access
# json not in sys.modules
# Any of these trigger reification:
dumps_func = json.dumps
dumps_func = getattr(json, 'dumps')
# Now json is in sys.modules
지연 임포트는 순환 임포트와 함께 작동합니까?
지연 임포트가 순환 임포트 문제를 자동으로 해결하지는 않습니다. 두 모듈에 순환 의존성이 있는 경우, 모듈 초기화 중에 순환 참조에 접근하지 않는 경우에만 도움이 될 수 있습니다. 그러나 어느 한 모듈이 import 시점에 다른 모듈에 접근하면 여전히 오류가 발생합니다.
작동하는 예제 (함수에서 접근 지연):
# user_model.py
lazy import post_model
class User:
def get_posts(self):
# OK - post_model accessed inside function, not during import
return post_model.Post.get_by_user(self.name)
# post_model.py
lazy import user_model
class Post:
@staticmethod
def get_by_user(username):
return f"Posts by {username}"
어느 모듈도 모듈 수준에서 다른 모듈에 접근하지 않기 때문에 작동합니다. get_posts()이 호출될 때 나중에 접근이 수행됩니다.
실패하는 예제 (import 중 접근):
# module_a.py
lazy import module_b
result = module_b.get_value() # Error! Accessing during import
def func():
return "A"
# module_b.py
lazy import module_a
result = module_a.func() # Circular dependency error here
def get_value():
return "B"
이는 module_a가 임포트 시점에 module_b에 접근하려고 하고, 그 결과 완전히 초기화되기 전에 module_a에 접근하려고 하므로 실패합니다.
여전히 가장 좋은 방법은 코드 설계에서 순환 임포트를 피하는 것입니다.
지연 임포트가 핫 패스의 성능에 영향을 줍니까?
첫 사용 이후에는(임포트가 성공한다면) 적응형 인터프리터 덕분에 지연 임포트에 오버헤드가 전혀 없습니다. 인터프리터는 바이트코드를 특수화하며(예: LOAD_GLOBAL이 LOAD_GLOBAL_MODULE이 됨), 이후 접근에서 지연 검사를 제거합니다. 즉, 지연 임포트가 한 번 실체화되면 이에 접근하는 속도는 일반 import와 똑같이 빠릅니다.
lazy import json
def use_json():
return json.dumps({"test": 1})
# First call triggers reification
use_json()
# After 2-3 calls, bytecode is specialized
use_json()
use_json()
dis.dis(use_json, adaptive=True)를 사용하여 특수화를 확인할 수 있습니다.
=== Before specialization ===
LOAD_GLOBAL 0 (json)
LOAD_ATTR 2 (dumps)
=== After 3 calls (specialized) ===
LOAD_GLOBAL_MODULE 0 (json)
LOAD_ATTR_MODULE 2 (dumps)
특수화된 LOAD_GLOBAL_MODULE 및 LOAD_ATTR_MODULE 명령어는 지연 임포트를 검사하는 오버헤드가 없는 최적화된 고속 경로입니다.
sys.modules는 어떻습니까? 지연 임포트는 언제 그곳에 나타납니까?
지연 임포트된 모듈은 실체화될 때까지(처음 사용될 때까지) sys.modules에 나타나지 않습니다. 실체화되면 일반 임포트와 마찬가지로 sys.modules에 나타납니다.
import sys
lazy import json
print('json' in sys.modules) # False
result = json.dumps({"key": "value"}) # First use
print('json' in sys.modules) # True
lazy from __future__ import feature가 작동합니까?
아니요. 미래 임포트는 파서/컴파일러 지시문이므로 지연 처리할 수 없습니다. 런타임 동작을 지연 처리하는 것은 기술적으로 가능하지만, 그렇게 해도 실질적인 이점은 없습니다.
lazy를 키워드 이름으로 선택한 이유는 무엇입니까?
“왜”가 아니라… 외우십시오! :)
보류된 아이디어
다음 아이디어들이 검토되었지만, 먼저 안정적이고 사용 가능한 핵심 기능을 제공하는 데 집중하기 위해 의도적으로 보류되었습니다. 지연 임포트에 대한 실제 사용 경험을 얻은 후 향후 개선 사항으로 검토할 수 있습니다.
대체 구문 및 사용 편의성 개선
사용 편의성을 개선하기 위해 몇 가지 대체 구문 형식이 제안되었습니다.
- 타입 전용 임포트: 타입 어노테이션에만 독점적으로 사용되는 임포트를 위한 특수 구문(다른 문맥의
type키워드와 유사)을 추가할 수 있습니다. 예를 들어type from collections.abc import Sequence와 같습니다. 이렇게 하면 타입 전용 임포트에lazy를 사용하는 것보다 의도가 더 명확해지고, 해당 임포트가 런타임에 절대 사용되지 않는다는 점을 독자에게 알릴 수 있습니다. 그러나lazy임포트가 이미 타입 어노테이션의 런타임 비용 문제를 해결하므로, 더 단순하고 일반적인 메커니즘으로 시작한 다음 사용 데이터를 수집한 후 특수 구문이 충분한 가치를 더하는지 평가하는 방식을 선호합니다. - 블록 기반 구문: 다음과 같이 여러 지연 임포트를 하나의 블록으로 묶는 방식입니다.
as lazy: import foo from bar import baz
이렇게 하면 많은 임포트를 지연 임포트로 표시할 때 반복을 줄일 수 있습니다. 그러나 Python의 기존 문법 패턴에 맞지 않는 완전히 새로운 문장 형식(
as lazy:블록)을 도입해야 합니다. 이것이 다른 언어 기능과 어떻게 상호 작용할지, 또는 유사한 블록 수준 수정자에 어떤 선례가 있을지는 명확하지 않습니다. 또한 이 방식은 코드를 훑어볼 때 특정 임포트가 지연 임포트인지 파악하기 어렵게 합니다. 임포트 줄 자체가 아니라 주변 문맥을 살펴봐야 하기 때문입니다.
이러한 대안은 특정 문맥에서 서로 다른 사용 편의성을 제공할 수 있지만, 새로운 문장 형식을 도입하거나 기존 구문을 명확하지 않은 방식으로 과도하게 활용해야 한다는 유사한 단점이 있으며, 언어를 상당히 확장할 수 있는 유사한 구문 패턴의 다른 잠재적 용도까지 열어 놓게 됩니다. 명시적인 lazy import구문으로 시작하고, 추가적인 구문 변형을 고려하기 전에 실제 사용 현장의 피드백을 수집하는 방식을 선호합니다. 향후 사용 편의성 개선은 추측에 기반한 이점이 아니라 실제 사용 패턴을 토대로 평가해야 합니다.
if TYPE_CHECKING블록을 위한 자동 지연 임포트
향후 개선 사항으로 if TYPE_CHECKING: 블록 내부의 모든 임포트를 자동으로 지연 임포트로 처리할 수 있습니다.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from foo import Bar # Could be automatically lazy
그러나 현재 TYPE_CHECKING은 단순한 런타임 변수이므로, 이를 컴파일 시점에 작동시키려면 상당한 변경이 필요합니다. 컴파일러는 from __future__ import 문을 처리하는 방식과 유사하게 이 패턴에 대한 특별한 지식을 갖추어야 합니다. 또한 안정적으로 작동하게 하려면 TYPE_CHECKING을 내장 기능으로 만들어야 합니다. lazy 임포트가 이미 타입 전용 임포트의 런타임 비용 문제를 해결하므로, 명시적 구문으로 시작한 다음 이 최적화가 충분한 가치를 더하는지 평가하는 방식을 선호합니다.
모듈 수준 지연 임포트 모드
해당 모듈의 모든 임포트를 기본적으로 지연 임포트로 만드는 모듈 수준 선언입니다:
from __future__ import lazy_imports
import foo # Automatically lazy
이는 여러 가지 질문을 제기하기 때문에 논의되었지만 보류되었습니다. from __future__ import을 사용하면 향후 Python 버전에서 이것이 기본 동작이 된다는 의미인데, 이는 명확하지 않으며 현재 계획되어 있지도 않습니다. 또한 이러한 모드가 전역 플래그와 어떻게 상호 작용할지, 전환 경로가 어떻게 될지에 대한 질문도 제기됩니다. 현재의 명시적 구문과 __lazy_modules__는 초기 도입에 충분한 제어 기능을 제공합니다.
지연 임포트 안전 선언을 위한 패키지 메타데이터
향후 개선을 통해 패키지가 지연 임포트에 안전한지(예: 임포트 시 부작용이 없는지)를 메타데이터에 선언할 수 있게 될 수 있습니다. 이는 필터 메커니즘이나 정적 분석 도구에서 사용할 수 있습니다. 현재 필터 API는 핵심 언어 사양을 변경하지 않고도 이러한 향후 추가 사항을 수용할 수 있도록 설계되었습니다.
지연 임포트를 위한 C API
지연 임포트를 생성하거나 확인하기 위한 전용 C API는 계획되어 있지 않습니다. C 확장은 일반적으로 모듈에 즉시 액세스해야 하며 지연 로딩의 이점을 얻을 수 없으므로, 이 기능은 순수하게 Python을 대상으로 하는 메커니즘으로 설계되었습니다. PyImport_ImportModule()와 같은 기존 C API 함수는 변경되지 않으며 계속해서 즉시 임포트를 수행합니다. 설득력 있는 사용 사례가 나타나면 향후 버전에서 이를 다시 검토할 수 있습니다.
대안적 구현 아이디어
다음은 이 PEP를 개발하는 동안 고려했던 몇 가지 대안적 설계 결정입니다. 현재 제안은 단순성, 성능 및 유지 관리 편의성 사이에서 최상의 균형이라고 판단되지만, 이러한 대안은 구현자가 고려하거나 향후 개선에 활용할 수 있는 서로 다른 절충점을 제공합니다.
dict의 서브클래스 활용
지연 임포트를 지원하는 데 필요한 필드를 내부 dict 객체에 직접 추가하는 대신, 지연 임포트 활성화에만 사용되는 dict 객체의 서브클래스를 만들 수 있습니다. 그러나 이는 여전히 누출되는 추상화입니다. 예를 들어 dict.__getitem__과 같은 메서드를 직접 호출할 수 있으며, 인터프리터에서 전역 변수 조회 성능에 영향을 미칩니다.
대안적 키워드 이름
이 PEP에서는 임포트 오버헤드 최적화에 이미 관심을 두고 있는 사람들에게 가장 익숙하게 느껴질 것이라고 판단하여 명시적 키워드로 lazy를 제안하기로 했습니다. 명시적 지연 임포트를 지원하기 위한 다양한 다른 옵션도 고려했습니다. 가장 유력한 대안은 defer와 delay였습니다.
거부된 아이디어
새로운 동작을 기본값으로 만들기
import를 기본적으로 지연 방식으로 변경하는 것은 이 PEP의 범위를 벗어납니다. 관련 PEP 690에 관한 논의를 보면 이것이 상당히 논쟁적인 발상임이 분명하지만, 지연 임포트가 널리 사용되고 나면 재고할 수 있을지도 모릅니다.
__lazy_modules__ = ["*"]를 내장 구문으로 지원하기
명시적으로 열거하지 않고 모듈의 모든 임포트를 지연 임포트로 만들 수 있는 편리한 방법으로 __lazy_modules__ = ["*"]을 지원하자는 제안이 검토되었습니다. 이 접근 방식은 __lazy_modules__가 이미 암묵적인 원격 작용 동작을 나타내며, 이는 오로지 하위 호환성 메커니즘으로만 용인되기 때문에 거부되었습니다. 와일드카드 패턴에 대한 지원을 확장하면 구현 복잡성이 크게 증가하고, 패턴 매칭 및 글로빙 기능으로 범위가 불필요하게 확장될 수 있습니다. 영구적인 언어 기능인 __lazy_modules__는 향후 버전에서 제거할 수 없으므로, 이 설계는 최소주의를 우선시하고 하위 호환성을 위한 과도기적 도구로서의 역할로 범위를 제한합니다.
구현은 __lazy_modules__ 객체에 대해 __contains__를 호출하여 멤버십 검사를 수행한다는 점에 유의할 필요가 있습니다. 따라서 와일드카드 동작이 필요한 사용자는 __contains__를 구현하여 모든 질의 또는 기타 원하는 패턴에 대해 True를 반환하는 사용자 정의 객체를 제공할 수 있습니다. 이 설계는 기본 메커니즘에 대한 단순하고 집중된 사양을 유지하면서 고급 사용 사례에 필요한 유연성을 제공합니다. 이 PEP가 승인되면 이러한 도우미 객체를 표준 라이브러리에 추가하는 방안을 향후 이슈에서 논의할 수 있습니다. 현재로서는 이 PEP의 범위에 포함되지 않습니다.
with 블록 내부에서 지연 임포트 금지
이 PEP의 이전 버전에서는 try 블록에 대한 제한과 유사하게 with 블록 내부에서 lazy import 문을 금지할 것을 제안했습니다. 우려 사항은 특정 컨텍스트 관리자(예: contextlib.suppress(ImportError)와 같은)가 지연 임포트와 함께 사용될 때 임포트 오류를 혼란스러운 방식으로 억제할 수 있다는 것이었습니다.
그러나 with 문은 try/except 블록보다 훨씬 광범위한 의미를 가지므로 이 제한은 거부되었습니다. try/except는 명시적으로 예외를 포착하기 위한 것이지만, with 블록은 일반적으로 리소스 관리, 임시 상태 변경 또는 범위 지정을 위해 사용되며, 이러한 상황에서는 지연 임포트가 아무 문제 없이 작동합니다. lazy import 구문은 충분히 명시적이므로, 이를 with 블록 내부에 작성하는 개발자는 의도적인 선택을 하는 것이며, 이는 Python의 “동의한 성인” 철학과 일치합니다. with suppress(ImportError): lazy import foo와 같이 실제로 문제가 될 수 있는 사례는 강력한 언어 제한보다 정적 분석 도구와 린터가 이러한 패턴을 찾아내는 데 더 적합합니다.
전역 플래그에서 with 블록의 임포트를 즉시 임포트로 강제하기
또 다른 거부된 아이디어는 전역 지연 임포트 플래그가 "all"로 설정되어 있어도 with 블록 내부의 임포트를 즉시 임포트로 유지하는 것이었습니다. 그 근거는 보수적으로 접근하자는 것이었습니다. with 문은 임포트 동작에 영향을 줄 수 있으므로(예: sys.path를 수정하거나 예외를 억제함), 임포트를 즉시 임포트로 유지하면 미묘한 버그를 방지할 수 있다는 것입니다. 그러나 이렇게 하면 with 블록에서 lazy import가 명시적으로 허용되는 반면, 전역 플래그가 활성화되었을 때 일반 임포트는 즉시 임포트로 유지되는 일관성 없는 동작이 발생합니다. 명시적 지연과 암묵적 지연 사이의 이러한 불일치는 혼란스럽고 설명하기 어렵습니다.
더 단순하고 일관된 규칙은 전역 플래그가 명시적 lazy import 구문이 허용되는 모든 곳의 임포트에 영향을 주도록 하는 것입니다. 이렇게 하면 세 가지 서로 다른 규칙 집합(명시적 구문, 전역 플래그 동작 및 필터 메커니즘)을 두는 대신 두 가지를 제공할 수 있습니다. 즉, 명시적 구문 규칙은 전역 플래그가 영향을 미치는 대상과 일치하고, 필터 메커니즘은 특수한 경우를 위한 우회 수단을 제공합니다. 세밀한 제어가 필요한 사용자의 경우 필터 메커니즘(sys.set_lazy_imports_filter())은 이미 특정 임포트 또는 패턴을 제외할 방법을 제공합니다. 또한 역연산이 없습니다. 전역 플래그가 with 블록에서 임포트를 즉시 임포트로 강제하지만 사용자가 이를 지연 임포트로 만들고 싶을 때 재정의할 방법이 없으므로 비대칭이 발생합니다.
요약하면, with 블록의 임포트는 lazy import로 명시적으로 표시되었는지 전역 플래그를 통해 암묵적으로 지정되었는지와 관계없이 일관되게 동작하므로, 설명하고 추론하기 쉬운 단순한 규칙이 됩니다.
딕셔너리 객체의 수정
지연 임포트를 위한 최초의 PEP(PEP 690)는 지연 임포트를 지원하기 위해 내부 딕셔너리 객체의 수정에 크게 의존했습니다. 이 데이터 구조가 고도로 최적화되어 있고 코드베이스 전반에서 광범위하게 사용되며 성능에 매우 민감하다는 점을 인식하고 있습니다. 이 데이터 구조가 중요하고, 기능에 관심이 없을 수 있는 사용자에게 지연 임포트 구현을 캡슐화된 상태로 제공하고자 하므로, 대안적 접근 방식에 투자하기로 결정했습니다.
딕셔너리는 Python의 기반 데이터 구조입니다. 모든 객체의 속성은 딕셔너리에 저장되며, 딕셔너리는 네임스페이스, 키워드 인자 등을 비롯한 런타임 전반에서 사용됩니다. 지연 임포트를 지원하기 위해 딕셔너리에 어떤 종류의 후크나 특수 동작을 추가하면 다음과 같은 문제가 발생합니다:
- 향후 JIT 컴파일을 비롯한 중요한 인터프리터 최적화를 방해합니다.
- 단순하고 빨라야 하는 데이터 구조에 복잡성을 추가합니다.
- 임포트 동작뿐만 아니라 Python의 모든 부분에 영향을 줍니다.
- 관심사의 분리를 위반합니다 – 해시 테이블은 임포트 시스템을 알 필요가 없습니다.
핵심 추상화를 깔끔하게 유지한다는 원칙을 위반한 과거의 결정은 CPython 생태계에 상당한 부담을 초래하여 최적화를 어렵게 만들고 미묘한 버그를 도입했습니다.
__class__ 변이를 통한 지연 객체 변환
객체를 완전히 교체하는 대신 내부 상태를 변경하여 지연 임포트 객체를 최종 형태로 변환하는 대안적 구현 방식이 제안되었습니다. 이 접근법에서는 실제 임포트가 완료된 후 지연 객체가 제자리에서 변환됩니다.
이 접근법은 다음과 같은 여러 이유로 거부되었습니다:
- 이 기법은 모듈 객체에는 잠재적으로 적용할 수 있지만,
from문을 통해 임포트되는 임의의 객체에는 완전히 적용할 수 없습니다. 사용자가lazy from foo import bar라고 작성할 때 객체bar는 모듈뿐만 아니라 어떤 Python 객체든 될 수 있습니다(함수, 클래스, 상수 등). 어떤 변환 방식이든 지연 프록시 객체가 대상 객체와 호환되는 메모리 레이아웃 및 기타 특성을 가져야 하지만, 모듈을 로드하기 전에는 이를 알 수 없습니다. 이로 인해lazy import x와lazy from x import y는 완전히 다른 구현 전략을 필요로 하는 근본적인 비대칭이 발생하며, 후자의 경우에도 프록시 교체 메커니즘이 필요합니다. - 모듈 객체의 경우에도 이 접근법에는 근본적인 한계가 있습니다. 일부 구현은 표준 모듈 형식을 상속하거나 이를 대체하는 사용자 정의 클래스를
sys.modules에 대입합니다. 이러한 사용자 정의 모듈 클래스는PyModuleObject와 메모리 레이아웃 및 크기가 다를 수 있습니다. 변환 방식은 이러한 일반적인 사용자 정의 모듈 구현과 함께 사용할 수 없으므로 생태계 전반에 취약성과 유지 관리 부담을 초래합니다. - 변환 방식이 일부 측면에서는 더 단순해 보일 수 있지만, 이는 다소 주관적인 판단입니다. 이 방식은 분기된 구현을 도입합니다. 모듈용 경로와 비모듈 객체용의 완전히 다른 경로가 생깁니다. 이것이 통합된 프록시 메커니즘보다 단순한지는 관점과 구현 세부 사항에 따라 달라집니다.
- 변환 전에 객체에 대한 참조를 보유한 모든 코드는 변환 후 다른 형식을 보게 됩니다. 이는 객체 형식을 확인하거나 형식 안정성에 의존하는 코드를 손상시킬 수 있으며, 특히 형식 포인터를 캐시하거나
PyObject_TypeCheck를 사용하는 C 확장에 문제가 될 수 있습니다. 또한 변환 과정 전체에서 객체가 유효한 상태로 유지되도록 지연 임포트 기능과 형식 시스템 간의 세심한 조정이 필요합니다. 현재의 프록시 기반 설계는 지연 프록시와 실제로 임포트된 객체 사이에 명확한 경계를 유지하여 이러한 문제를 피합니다.
LazyImportType 프록시 패턴을 통한 객체 교체를 사용하는 현재 설계는 import와 from ... import 문 모두에 균일하게 작동하는 일관된 메커니즘을 제공하면서, 지연 임포트 기능과 Python의 핵심 객체 모델 사이를 더욱 깔끔하게 분리합니다.
모듈을 로드하지 않고 lazy 임포트가 모듈을 찾게 하기
Python의 import 메커니즘은 모듈 찾기와 모듈 로드를 분리하며, 지연 임포트 구현은 기술적으로 로드 부분만 지연할 수 있습니다. 그러나 이 접근법은 몇 가지 중요한 이유로 거부되었습니다.
성능 향상의 상당 부분은 찾기 단계를 건너뛰는 데서 비롯합니다. 이 문제는 NFS 기반 파일 시스템과 분산 스토리지에서 특히 심각하며, 이러한 환경에서는 각각의 stat() 호출에 네트워크 지연 시간이 발생합니다. 이러한 환경에서는 네트워크 상태에 따라 stat() 호출에 수십 밀리초에서 수백 밀리초가 걸릴 수 있습니다. 수십 개의 임포트가 각각 여러 파일 시스템 검사를 수행하며 sys.path를 순회하면, Python 코드를 실행하기 전에 모듈을 찾는 데 소요되는 시간이 상당해질 수 있습니다. 일부 측정에서는 명세 찾기가 전체 임포트 시간의 대부분을 차지합니다. 로딩 단계만 건너뛰면 성능 문제의 대부분이 해결되지 않은 채 남습니다.
더 중요하게는, 찾기와 로딩을 분리하면 오류 처리에서 양쪽의 최악의 결과가 발생합니다. 임포트 메커니즘에서 발생하는 일부 예외(예: 모듈 누락으로 인한 ImportError, 경로 확인 실패, ModuleNotFoundError)는 lazy import 문에서 발생하는 반면, 다른 예외(예: SyntaxError, 순환 임포트로 인한 ImportError, from module import name에서 발생하는 속성 오류)는 나중에 처음 사용될 때 발생합니다. 이러한 분리는 혼란스럽고 예측하기 어렵습니다. 개발자는 어떤 오류가 언제 발생하는지 알기 위해 내부 임포트 메커니즘을 이해해야 합니다. 현재 설계는 더 단순합니다. 완전한 지연 임포트를 사용하면 모든 임포트 관련 오류가 처음 사용될 때 발생하므로 동작이 일관되고 예측 가능합니다.
또한 기술적 한계도 있습니다. 모듈을 찾는다고 해서 임포트가 성공한다는 보장은 없으며, ImportError를 발생시키지 않는다는 보장조차 없습니다. 패키지에서 모듈을 찾으려면 해당 패키지가 로드되어 있어야 하므로, 이는 패키지 계층 구조에서 한 단계만 지연 로딩하는 데 도움이 될 뿐입니다. 모듈에서 속성을 “찾는” 작업은 모듈을 로드해야 필요로 하므로, from package import module과 from module import function 사이에 설명하기 어려운 차이가 생깁니다.
from 임포트 중간에 lazy 키워드 배치
새로운 명시적 구문에서 from foo lazy import bar가 매우 직관적인 위치라고 생각했지만, 곧 이 위치에 lazy 키워드를 배치하는 것이 Python에서 이미 구문상 허용된다는 사실을 알게 되었습니다. 이는 from . lazy import bar가 올바른 구문이기 때문입니다(공백은 중요하지 않기 때문입니다).
임포트 문 끝에 lazy 키워드 배치
import foo lazy 또는 from foo import bar, baz lazy와 같이 임포트 문의 끝에 lazy를 덧붙이는 방안을 논의했지만, 결국 이 방식은 명확성이 떨어진다고 판단했습니다. 예를 들어 하나의 문에서 여러 모듈을 임포트하는 경우, lazy 바인딩이 임포트된 모든 객체에 적용되는지 아니면 항목의 일부에만 적용되는지 불분명합니다.
명시적 eager 키워드 추가
기본 동작을 변경하지 않을 것이고 전역 플래그의 사용을 권장하고 싶지도 않으므로, 일반적인 기본 사례에 불필요한 구문을 추가하는 방안을 고려하기에는 아직 이릅니다. 이렇게 하면 기본값이 무엇인지, eager 키워드가 언제 필요한지, 또는 명시적으로 eager 임포트된 모듈 내부의 지연 임포트에 영향을 주는지에 대해 지나치게 큰 혼란이 생길 수 있습니다.
전역적으로 비활성화된 경우에도 필터가 지연 임포트를 강제하도록 허용
지연 임포트는 그렇지 않으면 실패할 일부 형태의 순환 임포트를 의도적이고 바람직한 방식으로 허용하므로(특히 타이핑 관련 임포트에서 그러함), 전역 비활성화를 재정의하고 특정 임포트를 지연 임포트로 강제하는 방법을 추가하자는 제안이 있었습니다. 예를 들어 지연 임포트가 전역적으로 비활성화된 경우에도 지연 임포트 필터를 호출하는 방식입니다.
이 방식은 서로 다른 “재정의” 시스템의 복잡한 계층 구조를 도입하여 코드를 분석하고 추론하기 훨씬 어렵게 만들 수 있습니다. 또한 지연 임포트의 사용이 발전함에 따라 특정 임포트를 활성화하거나 비활성화하는 더 세분화된 시스템을 도입하려면 추가적인 복잡성이 필요할 수 있습니다. 전역 비활성화는 일반적으로 널리 사용될 것으로 예상되지 않으며, 지연 임포트에 대한 의존성을 엄격하게 통제하려는 사용자를 위한 디버깅 및 선택적 테스트 도구에 더 가깝습니다. 패키지 관리자가 패키지를 업데이트하여 지연 임포트를 도입할 때, 지연 임포트가 전역적으로 비활성화된 상태에서의 실행을 지원하지 않기로 결정하는 것은 합리적이라고 생각합니다.
이는 시간이 지나 더 많은 패키지가 타이핑과 지연 임포트를 모두 받아들이면 전역 비활성화가 대부분 사용되지 않고 사용할 수도 없게 된다는 의미일 수 있습니다. 과거에도 다른 전역 플래그에서 비슷한 일이 있었으며, 플래그를 추가하는 비용이 낮다는 점을 고려하면 이는 받아들일 수 있어 보입니다. 실제 사용 방식과 패턴을 더 명확히 파악한 후에 더 구체적인 재활성화 메커니즘을 추가하는 편이, 충분히 올바르지 않은 메커니즘을 성급하게 추가한 뒤 제거하는 것보다 쉽습니다.
고급 기능에 밑줄 접두사가 붙은 이름 사용
전역 활성화 및 필터 함수(sys.set_lazy_imports, sys.set_lazy_imports_filter, sys.get_lazy_imports_filter)는 밑줄 접두사를 사용하여 “비공개” 또는 “고급”으로 표시할 수 있습니다(예: sys._set_lazy_imports_filter). 문서를 통해 고급 기능임을 명시하는 것으로 충분하므로 이는 거부되었습니다. 이러한 함수는 고급 사용자, 특히 대규모 배포를 운영하는 사용자에게 정당한 사용 사례가 있습니다. 공식 메커니즘을 제공하면 업스트림 CPython과의 분기를 방지할 수 있습니다. 전역 모드는 일상적인 사용자나 라이브러리가 아니라, 거대한 플릿을 운영하는 사용자를 위한 고급 기능으로 의도적으로 문서화되어 있습니다. Python에는 밑줄 접두사 없이 공개 API로 유지되는 고급 기능의 선례가 있습니다. 예를 들어 gc.disable(), gc.get_objects(), gc.set_threshold()는 잘못 사용하면 문제를 일으킬 수 있는 고급 기능이지만 밑줄 접두사가 붙어 있지 않습니다.
지연 임포트에 데코레이터 구문 사용
데코레이터 기반 구문을 사용하면 임포트를 지연으로 표시할 수 있습니다.
@lazy
import json
@lazy
from foo import bar
이 접근 방식은 해결되지 않은 질문과 복잡성을 지나치게 많이 도입하므로 거부되었습니다. Python의 데코레이터는 문장이 아니라 호출 가능 객체(함수, 클래스, 메서드)를 감싸고 변환하도록 설계되었습니다. 임포트 문에 데코레이터를 허용하면 다른 여러 잠재적인 문 데코레이터(@cached, @traced, @deprecated 등)가 등장할 수 있으며, 이는 우리가 탐구하고 싶지 않은 방식으로 언어의 구문을 크게 확장하게 됩니다. 또한 이러한 데코레이터가 어디에서 제공되는지에 대한 문제가 제기됩니다. 데코레이터는 임포트되거나 내장되어야 하므로, 임포트 관련 데코레이터에 대한 부트스트랩 문제가 발생합니다. 이는 초점을 맞춘 lazy import 구문보다 훨씬 더 추측적이고 일반적입니다.
새로운 소프트 키워드 대신 컨텍스트 관리자 사용
예를 들어 컨텍스트 관리자 형태의 하위 호환 가능한 구문이 제안되었습니다.
with lazy_imports(...):
import json
이렇게 하면 __lazy_modules__의 필요성이 사라지고, 라이브러리가 이전 Python 버전에서 기존 지연 임포트 구현 중 하나를 사용할 수 있습니다. 그러나 이러한 효과를 내는 마법 같은 with문을 추가하는 것은 Python과 with문 전반에 중대한 변경을 가하는 것이며, 이 제안의 지연 임포트 구현과 결합하기도 쉽지 않습니다. 기존 지연 임포터에 대한 표준 라이브러리 지원을 구현 변경 없이 추가하는 것은 현 상태를 답습하는 것이며, 기존 솔루션의 성능 및 사용성 문제를 해결하지 못합니다.
globals()에서 프록시 딕셔너리 반환
globals()에서 실체화하거나 지연 객체를 노출하는 대신, 프록시를 통해 액세스할 때 지연 객체를 자동으로 실체화하는 프록시 딕셔너리를 반환할 수도 있습니다. 이는 양쪽의 장점을 모두 제공하는 것처럼 보입니다. globals()는 실체화 비용 없이 즉시 반환되지만, 결과를 통해 항목에 액세스하면 지연 임포트가 자동으로 해결됩니다.
그러나 이 접근 방식은 실제로 globals()가 사용되는 방식과 근본적으로 호환되지 않습니다. 많은 표준 라이브러리 함수와 내장 함수는 globals()가 프록시가 아니라 실제 dict 객체를 반환할 것으로 예상합니다.
exec(code, globals())에는 실제 딕셔너리가 필요합니다.eval(expr, globals())에는 실제 딕셔너리가 필요합니다.type(globals()) is dict를 검사하는 함수는 작동하지 않게 됩니다..update()와 같은 딕셔너리 메서드에는 특별한 처리가 필요합니다.- 모든 액세스에서 간접 참조가 발생하므로 성능이 저하됩니다.
프록시는 거의 모든 경우에 실제 딕셔너리와 구별할 수 없을 정도로 투명해야 하며, 이를 올바르게 구현하기는 매우 어렵습니다. 실제 딕셔너리 동작에서 벗어나는 모든 차이는 미묘한 버그의 원인이 됩니다.
__dict__ 또는 globals() 액세스 시 자동으로 실체화
지연 임포트에서 globals()와 mod.__dict__가 어떻게 동작해야 하는지에 대해 세 가지 옵션이 고려되었습니다.
globals()또는mod.__dict__를 호출하면 반환하기 전에 모든 지연 객체를 순회하고 구체화합니다.globals()또는mod.__dict__를 호출하면 지연 객체가 포함된 딕셔너리를 반환합니다(선택됨).globals()를 호출하면 지연 객체가 포함된 딕셔너리를 반환하지만,mod.__dict__는 모든 것을 구체화합니다.
옵션 2를 선택했습니다. globals()와 __dict__모두 구체화를 트리거하지 않고 원시 네임스페이스 딕셔너리를 반환합니다. 이를 통해 저수준 인트로스펙션 API가 부작용을 트리거하지 않는 깔끔하고 예측 가능한 모델을 제공합니다.
globals()와 __dict__가 동일하게 동작하도록 하면 대칭성과 단순한 멘탈 모델이 형성됩니다. 둘 다 원시 네임스페이스 뷰를 노출합니다. 저수준 인트로스펙션 API가 임포트를 자동으로 트리거해서는 안 됩니다. 이는 놀라울 수 있고 잠재적으로 비용이 많이 들기 때문입니다. 표준 라이브러리에서 지연 임포트를 구현한 실제 경험(예: traceback 모듈)은 __dict__접근 시 자동 구체화가 번거롭고, 인트로스펙션 코드가 검사만 하던 모듈을 로드하도록 강제한다는 점을 보여 주었습니다.
옵션 1(항상 구체화)은 globals()와 __dict__접근을 놀라울 정도로 비용이 많이 들게 하고 모듈의 지연 상태를 검사하지 못하게 하므로 거부되었습니다. 옵션 3은 외부 코드가 지연 객체를 보지 못하도록 “보호하기 위해” 처음에는 고려되었지만, 실제 사용 사례에서는 이 방법이 해결한 문제보다 더 많은 문제를 일으키는 것으로 나타났습니다. 특히 부작용을 트리거하지 않고 모듈을 검사해야 하는 표준 라이브러리 코드에서 그러했습니다.
감사의 말
Paul Ganssle, Yury Selivanov, Łukasz Langa, Lysandros Nikolaou, Pradyun Gedam, Mark Shannon, Hana Joo와 Python Google 팀, Meta의 Python 팀, HRT의 Python 팀, Bloomberg Python 팀, Scientific Python 커뮤니티, PEP 690의 초기 논의에 참여한 모든 분, 그리고 이 PEP의 형성에 도움이 된 귀중한 피드백과 통찰을 제공해 주신 많은 분께 감사드립니다.
각주
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.