Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

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

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at Lazy imports.

×

See PEP 1 for how to propose changes.

초록

이 PEP는 지연 임포트를 명시적인 언어 기능으로 제공하는 문법을 도입합니다:

lazy import json
lazy from json import dumps

지연 임포트는 임포트한 이름이 처음 사용될 때까지 모듈의 로딩과 실행을 미룹니다. 반면 ‘일반’ 임포트는 import 문이 있는 지점에서 모듈을 즉시 로드하고 실행합니다.

개발자가 명시적 문법으로 개별 임포트를 지연 임포트로 표시할 수 있게 하면 Python 프로그램의 시작 시간, 메모리 사용량, 불필요한 작업을 줄일 수 있습니다. 이는 명령줄 도구, 테스트 스위트, 의존성 그래프가 큰 애플리케이션에 특히 유익합니다.

이 제안은 하위 호환성을 완전히 유지합니다: 일반 import 문은 그대로이며, 지연 임포트는 명시적으로 요청한 곳에서만 활성화됩니다.

동기

Python 코드에서는 보통 모든 임포트를 모듈 수준에 두며, 일반적으로 파일 맨 앞에 둡니다. 이렇게 하면 반복을 피하고, 임포트 의존성이 명확해지며, import 문을 모듈당 한 번만 평가하므로 런타임 오버헤드가 최소화됩니다.

이 방식의 큰 단점은 Python을 실행할 때 첫 번째 모듈(“main” 모듈)을 임포트하면 연쇄적인 임포트가 곧바로 일어나는 경우가 많고, 한 번도 쓰이지 않을 수 있는 많은 의존성까지 미리 로드한다는 점입니다. 이 영향은 하위 명령이 여러 개인 명령줄 도구에서 특히 큰 비용을 초래합니다. 이런 도구에서는 명령을 --help로 실행하기만 해도 불필요한 모듈 수십 개를 로드하느라 몇 초가 걸릴 수 있습니다. 이 기본적인 예는 프로그램을 실행하는 방법에 대한 유용한 안내를 사용자에게 보여 주는 것만을 위해서도 무엇을 로드해야 하는지 보여 줍니다. 비효율적이게도, 사용자는 원하는 명령을 알아낸 뒤 프로그램을 “실제로” 호출할 때 이 오버헤드를 다시 겪게 됩니다.

임포트를 지연하는 꽤 흔한 방법은 임포트를 함수 안으로 옮기는 것(인라인 임포트)입니다. 하지만 이 방식은 구현하고 유지 관리하는 데 더 많은 작업이 필요하며, 실수로 추가한 최상위 임포트 하나 때문에 무력화될 수 있습니다. 게다가 이 방식은 모듈의 전체 의존성 집합을 파악하기 어렵게 만듭니다. Python 표준 라이브러리를 분석하면 테스트를 제외한 전체 임포트의 약 17%(730개 파일에 걸쳐 총 3500개 가까운 임포트)가 실행을 늦추려는 목적으로 이미 함수나 메서드 안에 들어 있습니다. 이는 개발자들이 성능에 민감한 코드에서 이미 지연 임포트를 수동으로 구현하고 있음을 보여 줍니다. 하지만 그렇게 하려면 코드베이스 곳곳에 임포트를 흩어 놓아야 하고, 전체 의존성 그래프를 한눈에 이해하기가 더 어려워집니다.

표준 라이브러리는 이러한 비효율 문제 중 일부를 해결하기 위해 LazyLoader 클래스를 제공합니다. 이 클래스를 사용하면 모듈 수준의 임포트가 대체로 인라인 임포트처럼 동작합니다. 많은 과학 파이썬 라이브러리가 비슷한 패턴을 채택했으며, 이 패턴은 SPEC 1에서 공식화되었습니다. 지연 임포트의 또 다른 구현인 서드파티 lazy_loader 패키지도 있습니다. 정적 타입 검사에만 쓰이는 임포트도 불필요할 수 있는 임포트의 또 다른 원천이며, 그 오버헤드를 최소화하는 방법 역시 제각각입니다. 즉시 임포트를 지연하거나 제거하기 위해 여기서 사용하는 여러 방법은 범용 지연 임포트 메커니즘이 다룰 수 있는 사용 사례를 모두 포괄하지 못합니다. 명확한 표준이 없으며, 예상치 못한 곳에서 생기는 런타임 오버헤드나 더 나빠진 런타임 인트로스펙션을 비롯한 여러 단점이 있습니다.

이 제안은 지역적이고, 명시적이며, 통제되고, 세분화된 설계로 지연 임포트 문법을 도입합니다. 이 특성들은 하나하나가 이 기능을 실제로 예측 가능하고 안전하게 사용할 수 있게 하는 데 꼭 필요합니다.

동작은 지역적입니다: 지연은 lazy 키워드로 표시한 특정 임포트에만 적용되며, 다른 임포트로 재귀적으로 전파되지 않습니다. 따라서 개발자는 임포트된 모듈 자체가 다르게 동작할지 걱정할 필요 없이, 눈앞의 코드 한 줄만 보고도 지연의 효과를 추론할 수 있습니다. lazy import는 사용할 때마다 따로 내리는 결정이며, 의미 체계를 전역적으로 바꾸는 것이 아닙니다.

의미론은 명시적입니다. 이름을 지연 임포트하면 임포트하는 모듈에 바인딩이 즉시 만들어지지만, 대상 모듈은 그 이름에 처음 접근할 때까지 로드되지 않습니다. 그 이후로는 이 바인딩을 일반 임포트로 만들어진 바인딩과 구별할 수 없습니다. 이렇게 동작이 명확하면 예상치 못한 동작이 줄어들고, Python의 임포트 메커니즘을 깊이 알지 못하는 개발자도 이 기능을 쉽게 사용할 수 있습니다.

지연 임포트는 지연 로딩이 오직 임포트하는 코드 자신에 의해서만 발생한다는 의미에서 통제됩니다. 일반적인 경우, 라이브러리는 그 라이브러리의 작성자가 직접 지연 임포트로 표시하기로 선택한 경우에만 지연 임포트를 겪게 됩니다. 이를 통해 하위 사용자에게 책임이 전가되는 것을 피하고, 라이브러리 동작에서 뜻하지 않은 문제가 생기는 것을 막습니다. 라이브러리 작성자는 대개 자신의 임포트 하위 그래프를 직접 관리하므로, 지연을 언제 어떻게 적용할지에 대해 예측 가능한 통제권을 유지합니다.

이 메커니즘은 또한 세분화되어 있습니다. 이 메커니즘은 전역 플래그나 암묵적인 설정이 아니라 개별 임포트에 붙이는 명시적인 문법을 통해 도입됩니다. 따라서 개발자는 코드베이스에서 성능에 가장 민감한 영역부터 시작하여 점진적으로 이 기능을 도입할 수 있습니다. 이 기능을 커뮤니티에 소개하면서, 저희는 도입 경험이 선택적이고 점진적이며 각 프로젝트의 필요에 맞게 조정할 수 있기를 바랍니다.

지연 임포트는 다음과 같은 몇 가지 구체적인 이점을 제공합니다:

  • 명령줄 도구는 사용자가 직접 호출하는 경우가 많으므로 지연 시간이 – 특히 시작 지연 시간이 – 꽤 두드러지게 느껴집니다. 또한 이러한 프로그램은 (예를 들어 웹 서버와 달리) 일반적으로 수명이 짧은 프로세스입니다. 지연 임포트를 사용하면 실제로 도달한 코드 경로에서만 모듈을 임포트합니다. 실제로 시작 시간을 50~70% 줄일 수 있습니다. 그러면 흔히 겪는 사용자 경험이 크게 나아지고, 빠른 시작이 가장 중요한 분야에서 Python의 경쟁력도 높아집니다.
  • 타입 어노테이션에는 런타임에 전혀 사용되지 않는 임포트가 필요한 경우가 많습니다. 흔히 쓰는 우회 방법은 이러한 임포트를 if TYPE_CHECKING: 블록으로 감싸는 것입니다 [1]. 지연 임포트를 사용하면 어노테이션에만 쓰이는 임포트가 런타임 비용을 전혀 발생시키지 않으므로, 이러한 보호 블록이 필요 없어지고 어노테이션이 달린 코드베이스가 더 깔끔해집니다.
  • 대규모 애플리케이션은 수천 개의 모듈을 임포트하는 경우가 많으며, 각 모듈은 함수 객체와 타입 객체를 만들어 메모리 비용을 발생시킵니다. 오래 실행되는 프로세스에서는 이로 인해 기본 메모리 사용량이 눈에 띄게 늘어납니다. 지연 임포트는 모듈이 필요해질 때까지 이러한 비용을 미루어, 사용하지 않는 하위 시스템이 로드되지 않은 상태로 유지되게 합니다. 실제 워크로드에서 30-40%의 메모리 절감 효과가 관찰되었습니다.

근거

이 제안의 설계는 명확성, 예측 가능성, 도입 용이성에 중점을 둡니다. 각 결정은 언어나 그 런타임에 불필요한 복잡성을 더하지 않으면서도 지연 임포트가 실질적인 이점을 제공하도록 하기 위해 내려졌습니다.

또한 이 PEP는 하나의 구체적인 접근 방식을 제시하지만, 제안의 몇 가지 핵심 측면과 의미론에 대해서는 대안적인 구현 전략도 함께 제시한다는 점을 밝혀 둡니다. 커뮤니티가 동일한 핵심 의미론을 유지하는 다른 기술적 경로를 강하게 선호하거나 특정 선택지에 대해 근본적인 이견이 있는 경우를 대비하여, 이 제안을 준비하면서 이미 진행한 브레인스토밍 내용을 참고 자료로 포함했습니다.

새로운 lazy 키워드를 도입하기로 한 것은 명시적 문법이 필요하기 때문입니다. 지연 임포트는 일반 임포트와 의미가 다릅니다. 오류와 부작용이 임포트 문에서가 아니라 처음 사용할 때 발생합니다. 이러한 의미상의 차이 때문에 지연 여부가 전역 설정이나 멀리 떨어진 모듈 수준 선언에 숨겨지지 않고 임포트하는 위치 자체에서 드러나는 것이 매우 중요합니다. lazy 키워드를 사용하면 임포트 동작을 국소적으로 추론할 수 있으므로, 임포트가 지연되는지 알기 위해 코드의 다른 곳을 찾아볼 필요가 없습니다. 나머지 임포트 의미는 바뀌지 않습니다. 동일한 임포트 장치, 모듈 탐색 및 로딩 메커니즘을 사용합니다.

또 다른 중요한 결정은 딕셔너리 조회를 수정하는 대신 모듈의 네임스페이스에 있는 프락시 객체로 지연 임포트를 표현하는 것입니다. 이전 접근 방식에서는 딕셔너리에 지연 기능을 내장하는 방법을 실험했지만, 이 방법은 추상화의 경계를 흐리고 런타임의 관련 없는 부분에까지 영향을 줄 위험이 있었습니다. 딕셔너리는 Python의 기본 자료 구조이므로(말 그대로 모든 객체가 dict를 바탕으로 만들어집니다) 딕셔너리에 훅을 추가하면 중요한 최적화를 할 수 없게 되고 런타임 전체가 복잡해집니다. 프락시 방식은 더 단순합니다. 처음 사용될 때까지는 자리 표시자처럼 동작하다가, 그 시점에 임포트를 해결하고 이름을 다시 바인딩합니다. 그 이후부터는 이 바인딩을 일반 임포트와 구별할 수 없습니다. 따라서 이 메커니즘은 설명하기 쉽고, 인터프리터의 나머지 부분은 바뀌지 않은 채로 유지됩니다.

라이브러리 작성자를 위한 호환성 역시 핵심적인 고려 사항이었습니다. 많은 유지 관리자에게는 새 버전과 이전 버전의 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 문은 잠재적 지연 대상입니다. 지연될 수 없는 임포트는 전역 지연 임포트 플래그의 영향을 받지 않으며, 대신 항상 즉시 수행됩니다. 또한 from __future__ import 문은 지연될 수 없습니다.

문법 오류 예:

# 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 키워드를 사용하면 해당 임포트는 잠재적 지연 대상이 됩니다(고급 재정의 메커니즘은 Lazy imports filter를 참고하십시오). 모듈은 import 문에서 즉시 로드되지 않습니다. 대신 지연 프록시 객체가 생성되어 해당 이름에 바인딩됩니다. 실제 모듈은 그 이름을 처음 사용할 때 로드됩니다.

lazy from ... import를 사용하면 가져온 각 이름이 지연 프록시 객체에 바인딩됩니다. 이 이름들 중 어느 것에든 처음 접근하면 전체 모듈의 로딩이 시작되고, 해당 이름만 실제 값으로 구체화됩니다. 나머지 이름은 접근될 때까지 지연 프록시로 남아 있습니다. 인터프리터의 적응형 특수화는 몇 번의 접근 후 지연 검사를 최적화하여 제거합니다.

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__ 변수를 정의할 수 있으며, 이 변수는 어떤 모듈 이름을 (lazy 키워드를 사용한 것처럼) 잠재적 지연 대상으로 만들지 지정합니다. 이 변수는 각 import 문에서 검사되어 해당 임포트를 잠재적 지연 대상으로 만들지 결정하는 데 사용됩니다. 이 검사는 임포트되는 완전히 정규화된 모듈 이름을 담은 문자열을 인자로 __lazy_modules__ 객체의 __contains__를 호출하여 수행합니다. 일반적으로 __lazy_modules__는 완전히 한정된 모듈 이름 문자열의 집합입니다. 이 방식으로 모듈이 지연되면 해당 모듈을 사용하는 from-import도 지연되지만, 하위 모듈의 임포트까지 반드시 지연되는 것은 아닙니다.

일반(지연되지 않는) import 문은 전역 지연 임포트 플래그를 확인합니다. 플래그가 “all”이면 모든 임포트가 잠재적 지연 대상입니다(앞서 언급한 지연될 수 없는 임포트는 제외합니다).

예시

__lazy_modules__ = ["json"]
import json
print('json' in sys.modules)  # False
result = json.dumps({"hello": "world"})
print('json' in sys.modules)  # True

전역 지연 임포트 플래그가 “none”으로 설정되면 잠재적 지연 임포트도 결코 지연 방식으로 임포트되지 않으며, 동작은 일반 import 문과 동일합니다. 즉, 임포트는 (lazy 키워드를 사용하지 않은 것처럼) 즉시 수행됩니다.

마지막으로, 애플리케이션은 모든 잠재적 지연 임포트에 사용자 정의 필터 함수를 적용하여 각 임포트를 지연할지 여부를 결정할 수 있습니다(이는 고급 기능이며, Lazy imports filter를 참조하십시오). 필터 함수가 설정되어 있으면 임포트를 수행하는 모듈의 이름, 임포트되는 모듈의 이름, 그리고 (해당하는 경우) fromlist를 인자로 하여 필터 함수가 호출됩니다. 필터 함수가 True를 반환하는 경우에만 임포트가 지연 상태로 유지됩니다. 지연 임포트 필터가 설정되어 있지 않으면 모든 잠재적 지연 임포트가 지연됩니다.

.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__를 호출하며, 이때 lazy import 문이 평가된 시점의 상태가 아니라 구체화 시점의 임포트 시스템 상태(예: sys.path, sys.meta_path, sys.path_hooks, __import__)를 사용합니다.

모듈이 구체화되면 (아직 구체화되지 않은 다른 지연 참조가 남아 있더라도) sys.lazy_modules에서 제거됩니다. 패키지가 구체화될 때 그 패키지의 하위 모듈도 앞서 지연 임포트되었다면, 그 하위 모듈은 자동으로 구체화되지 않지만 구체화된 패키지의 전역에는 추가됩니다(다만 패키지가 이미 그 하위 모듈 이름에 다른 것을 대입했다면 추가되지 않습니다).

구체화가 실패하면(예: ImportError 때문에) 지연 객체는 구체화되거나 교체되지 않습니다. 이후 지연 객체를 사용하면 구체화를 다시 시도합니다. 구체화 중에 발생한 예외는 평소와 같이 발생하지만, 예외 체이닝을 통해 지연 임포트가 정의된 위치와 접근된 위치를 모두 보여 주도록 보강됩니다(예외 자체는 구체화를 촉발한 코드에서 전파되더라도 마찬가지입니다). 이를 통해 명확한 디버깅 정보를 얻을 수 있습니다:

# 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'?

이 예외 체이닝은 다음을 명확하게 보여 줍니다:

  1. 지연 임포트가 정의된 위치,
  2. 모듈이 즉시 임포트되지 않았다는 사실, 그리고
  3. 오류를 일으킨 실제 접근이 발생한 위치.

이전에 지연 임포트된 모듈이 이후에 즉시 임포트되더라도 구체화는 자동으로 일어나지 않습니다. 구체화는 해당 모듈을 참조하는 모든 지연 객체(예: lazy from 문)를 즉시 해석하지 않습니다. 구체화는 접근되는 지연 객체만 해석합니다.

지연 객체에 (전역 변수나 모듈 속성을 통해) 접근하면 그 객체가 구체화됩니다.

그러나 globals()를 호출하거나 모듈의 __dict__에 접근해도 구체화는 일어나지 않습니다 – 이들은 모듈의 딕셔너리를 반환하며, 그 딕셔너리를 통해 지연 객체에 접근하면 여전히 지연 프록시 객체가 반환되므로 사용할 때 수동으로 구체화해야 합니다. 지연 객체는 resolve 메서드를 호출하여 명시적으로 해석할 수 있습니다. 전역 스코프에서 dir()를 호출해도 전역 변수가 구체화되지 않으며, dir(mod)를 호출해도 (mod.__dir__의 특수 처리를 통해) 마찬가지입니다. 임의의 전역 변수에 접근하는 그 밖의 더 간접적인 방법(예: 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 명령어(모듈 및 클래스 수준에서 사용)는 모두 로드되는 객체가 지연 임포트인지 확인합니다. 지연 임포트라면 _PyImport_LoadLazyImportTstate()를 호출하여 실제 임포트를 수행하고 모듈을 sys.modules에 저장합니다.

이 검사는 접근할 때마다 아주 작은 비용을 발생시킵니다. 하지만 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) -> bool
  • sys.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 문과 같습니다. 즉, 임포트는 (lazy 키워드를 사용하지 않은 것처럼) 즉시 수행됩니다.

Python 코드에서 sys.set_lazy_imports() 함수를 실행하면 환경이나 CLI에서 물려받은 전역 지연 임포트 플래그의 상태를 바꿀 수 있습니다. 이는 애플리케이션이 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에 나타나야 합니다. 처음 사용하기 전에 다른 코드가 같은 모듈을 즉시 임포트하면, 지연 바인딩은 처음 사용될 때 이미 존재하는 해당 (지연) 모듈 객체로 해석됩니다.
  • 프락시 가시성. 처음 사용하기 전에는 바인딩된 이름이 지연 프락시를 가리킵니다. 값에 접근하는 간접적인 인트로스펙션에서는 프락시인 지연 객체 표현이 관찰될 수 있습니다. 처음 사용한 후에는 (모듈이 성공적으로 임포트되었다면) 이름이 실제 객체에 다시 바인딩되어 즉시 임포트와 구별할 수 없게 됩니다.

스레드 안전성과 구체화

구체화는 기존 임포트 잠금 규칙을 따릅니다. 정확히 하나의 스레드가 임포트를 수행하고, 임포트하는 모듈의 전역 변수를 해석된 객체에 원자적으로 다시 바인딩합니다. 그 이후에 동시에 읽는 스레드들은 실제 객체를 관찰합니다.

지연 임포트는 스레드 안전하며, 자유 스레딩(free-threading)과 관련하여 특별히 고려할 사항이 없습니다. 일반적으로 메인 스레드에서 임포트될 모듈이라도, 다른 스레드가 지연 임포트에 처음 접근하면 그 스레드에서 임포트될 수 있습니다. 이는 문제가 되지 않습니다. 어느 스레드가 임포트를 수행하든 임포트 잠금이 스레드 안전성을 보장합니다.

서브인터프리터를 지원합니다. 각 서브인터프리터는 자체 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%

네 가지 구성은 다음과 같습니다:

  1. 즉시 임포트(기준선): 지연 메커니즘이 없는 일반 Python 임포트입니다. 표준 Python 동작입니다.
  2. 지연 + 즉시 실행을 강제하는 필터: 필터 함수가 모든 임포트에 대해 False를 반환하여 즉시 실행을 강제한 다음, 스크립트 끝에서 모든 임포트를 구체화합니다. 모든 임포트가 필터를 거치지만 즉시 실행되므로, 순수한 필터 호출 오버헤드를 측정합니다.
  3. 지연 + 지연을 허용하는 필터 + 구체화: 필터 함수가 모든 임포트에 대해 True를 반환하여 지연 실행을 허용합니다. 스크립트 끝에서 모든 임포트를 구체화합니다. 임포트가 실제로 지연될 때의 필터 오버헤드를 측정합니다.
  4. 지연 + 필터 없음 + 구체화: 필터를 설치하지 않으며, 임포트는 지연되었다가 스크립트 끝에서 구체화됩니다. 필터가 없는 지연 동작의 기준선입니다.

벤치마크는 hyperfine을 사용하여 표준 라이브러리 모듈 278개를 테스트했습니다. 각 벤치마크는 새로운 Python 프로세스에서 실행되었습니다. 공정한 비교를 위해 모든 구성은 정확히 같은 모듈 집합(즉시 임포트 기준선이 로드하는 모든 모듈)을 강제로 임포트합니다.

벤치마크 환경에서는 32개의 논리 CPU(0-15는 3200 MHz, 16-31은 2400 MHz)에 CPU 격리를 적용하고, performance 스케일링 거버너를 사용했으며, Turbo Boost를 비활성화하고, ASLR 무작위화를 완전히 적용했습니다. 오버헤드 오차 막대는 (value - baseline) / baseline 공식에 표준 오차 전파를 적용하여 계산했으며, 측정값과 기준선 양쪽의 불확실성을 모두 반영합니다.

시작 시간 개선

지연 임포트의 주된 성능상 이점은 시작 시점에 전체 의존성 트리를 낙관적으로 로드하는 대신 런타임에 실제로 사용하는 모듈만 로드하여 시작 시간을 줄이는 것입니다.

대규모 실제 배포 사례에서 그 이점이 막대할 수 있음이 입증되었습니다. 물론 이는 구체적인 코드베이스와 사용 패턴에 따라 달라집니다. 규모가 크고 서로 긴밀히 연결된 코드베이스를 보유한 조직들은 서버 재로드 시간, ML 학습 초기화, 명령줄 도구 시작, Jupyter 노트북 로딩 시간이 상당히 줄었다고 보고했습니다. 사용하지 않는 모듈은 로드되지 않은 상태로 남으므로 메모리 사용량 개선도 관찰되었습니다.

프로덕션 배포의 자세한 사례 연구와 성능 데이터는 다음을 참고하십시오:

이점은 코드베이스의 복잡도에 비례하여 커집니다. 코드베이스가 크고 서로 긴밀히 연결되어 있을수록 개선 효과가 더 극적입니다. 특히 PySide 구현은 초기화 오버헤드가 큰 프레임워크가 옵트인 방식의 지연 로딩으로 상당한 이점을 얻을 수 있음을 잘 보여 줍니다.

타입 지정과 도구

타입 검사기와 정적 분석기는 이름 해석을 위해 lazy 임포트를 일반 임포트처럼 취급할 수 있습니다. 런타임에는 어노테이션에만 쓰이는 임포트를 lazy로 표시하여 시작 오버헤드를 피할 수 있습니다. IDE와 디버거는 최초 사용 전에는 지연 프록시를, 그 이후에는 실제 객체를 표시할 수 있도록 대비해야 합니다.

보안 영향

같은 환경에서 임포트를 수행하면서 패키지를 설치하는 도구는 새로 설치된 배포 패키지가 기존 모듈을 가리는 일이 없도록, 설치 단계 전에 모든 모듈을 즉시 임포트하거나 구체화(reify)해야 합니다.

이러한 도구는 sys.set_lazy_imports()에 "none"을 전달하여 즉시 평가를 강제하거나, 세밀한 제어를 위해 sys.set_lazy_imports_filter() 함수를 제공할 수 있습니다.

교육 방법

새로운 lazy 키워드는 언어 표준의 일부로 문서화될 것입니다.

이 기능은 옵트인 방식이므로, 새로운 Python 사용자는 익숙한 방식 그대로 언어를 계속 사용할 수 있어야 합니다. 숙련된 개발자는 위에 나열한 다양한 이점(지연 시간 감소, 메모리 사용량 감소 등)을 얻기 위해 상황에 따라 지연 임포트를 활용할 것으로 예상합니다. Python 바이너리의 성능에 관심이 있는 개발자는 프로파일링을 활용하여 코드베이스의 임포트 시간 오버헤드를 파악하고 필요한 임포트를 lazy로 표시할 가능성이 높습니다. 또한 개발자는 타입 어노테이션에만 사용될 임포트를 lazy로 표시할 수 있습니다.

Python 문서에는 지침, 전용 how-to 가이드, 임포트 시스템 문서 갱신 등 추가 문서가 더해질 것이며, 다루는 내용은 다음과 같습니다: 프로파일링 도구(-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; foo.bar.Baz가 아니라 항상 import foo.bar; 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
    
  • 와일드카드(별표) 임포트는 항상 즉시 수행되므로 사용하지 마십시오.

FAQ

거부된 PEP 690과는 어떻게 다릅니까?

PEP 810은 PEP 690의 암묵적인 전역 방식 대신 명시적인 옵트인 방식을 취합니다. 주요 차이점은 다음과 같습니다:

  • 명시적 문법: lazy import foo는 어떤 임포트가 지연 임포트인지 명확하게 표시합니다.
  • 지역적 범위: 지연은 해당 import 문에만 영향을 미치며, 의존성으로 연쇄적으로 전파되지 않습니다.
  • 더 단순한 구현: 핵심 딕셔너리 동작을 수정하는 대신 프록시 객체를 사용합니다.

구체화 시점에는 무엇이 바뀝니까? 무엇이 그대로 유지됩니까?

바뀌는 것 (시점):

  • 언제 모듈이 임포트되는지 - 임포트 문에서가 아니라 처음 사용할 때로 미뤄집니다
  • 언제 임포트 오류가 발생하는지 - 임포트 시점이 아니라 처음 사용할 때 발생합니다
  • 언제 모듈 수준의 부작용이 실행되는지 - 임포트 시점이 아니라 처음 사용할 때 실행됩니다

그대로 유지되는 것 (그 밖의 모든 것):

  • 사용되는 임포트 메커니즘 - 같은 __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은 다음을 수행합니다:

  1. module.py 파일 전체를 로드하고 실행합니다
  2. 결과로 얻은 모듈 객체에서 Class 속성을 추출합니다
  3. 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가 지연 방식과 즉시 방식으로 모두 임포트되면, 즉시 임포트가 우선하며 두 바인딩 모두 같은 모듈 객체로 해석됩니다.

기존 코드가 지연 임포트를 사용하도록 하려면 어떻게 마이그레이션합니까?

마이그레이션은 점진적으로 진행합니다:

  1. 프로파일링 도구를 사용하여 로드가 느린 모듈을 찾으십시오.
  2. 즉시 필요하지 않은 임포트에 lazy 키워드를 추가하십시오.
  3. 부작용이 일어나는 시점이 바뀌어도 기능이 깨지지 않는지 테스트하십시오.
  4. 이전 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__에 있는지 확인합니다. 있다면 해당 임포트는 lazy 키워드가 붙은 것처럼 처리됩니다(잠재적으로 지연 상태가 됩니다). 지연 임포트를 지원하지 않는 3.15 이전 Python 버전에서는 __lazy_modules__ 속성이 그냥 무시되고 임포트는 평소처럼 즉시 진행됩니다.

이는 lazy 키워드에 의존할 수 있게 될 때까지의 마이그레이션 경로를 제공합니다. 예측 가능성을 최대화하려면 __lazy_modules__를 모든 임포트보다 앞에서 한 번 정의하는 것이 좋습니다. 하지만 임포트할 때마다 확인하므로 import 문 사이에서 수정할 수도 있습니다.

명시적 지연 임포트는 PEP 649 및 PEP 749와 어떻게 상호 작용합니까?

Python 3.14는 PEP 649와 PEP 749에 명시된 대로 어노테이션의 지연 평가를 구현했습니다. 어노테이션이 문자열화되지 않았다면 그것은 나중에 평가되는 표현식입니다. 어노테이션에 접근할 때만 해석됩니다. 아래 예제에서 fake_typing 모듈은 사용자가 __annotations__ 딕셔너리를 조사할 때만 로드됩니다. 사용자가 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()를 통해 지연 임포트에 접근하면 접근한 속성의 구체화(reification)가 일어납니다. 모듈에 대해 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

지연 임포트는 순환 임포트에서도 동작합니까?

지연 임포트가 순환 임포트 문제를 자동으로 해결해 주지는 않습니다. 두 모듈이 순환 의존성을 가질 때, 임포트를 지연으로 만드는 것은 모듈 초기화 중에 순환 참조에 접근하지 않는 경우에만 도움이 될 수 있습니다. 하지만 두 모듈 중 하나라도 임포트 시점에 다른 모듈에 접근하면 여전히 오류가 발생합니다.

동작하는 예제 (함수 안에서의 지연 접근):

# 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()가 호출될 때 일어납니다.

실패하는 예제 (임포트 중 접근):

# 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_b가 다시 module_a가 완전히 초기화되기 전에 module_a에 접근하려 하기 때문입니다.

여전히 가장 좋은 방법은 코드를 설계할 때 순환 임포트를 피하는 것입니다.

지연 임포트가 핫 패스의 성능에 영향을 줍니까?

첫 사용 이후에는(임포트가 성공했다면) 적응형 인터프리터 덕분에 지연 임포트의 오버헤드가 전혀 없습니다. 인터프리터는 바이트코드를 특수화하며(예: LOAD_GLOBAL이 LOAD_GLOBAL_MODULE이 됩니다), 이를 통해 이후 접근에서는 지연 여부 검사가 사라집니다. 즉, 지연 임포트가 한 번 실체화되면 그에 접근하는 속도는 일반 임포트와 똑같이 빠릅니다.

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__는 이미 암묵적인 원격 작용(action-at-a-distance) 동작에 해당하며, 이는 오로지 하위 호환성 메커니즘으로서만 용인되는 것이기 때문입니다. 와일드카드 패턴까지 지원을 확장하면 구현 복잡도가 크게 늘어나고, 패턴 매칭과 글로빙 기능으로까지 범위가 점점 불어날 여지가 생깁니다. __lazy_modules__는 향후 버전에서 제거할 수 없는 영구적인 언어 기능이므로, 이 설계는 최소주의를 우선시하여 그 범위를 하위 호환성을 위한 과도기적 도구 역할로 제한합니다.

구현은 __lazy_modules__ 객체의 __contains__를 호출하여 멤버십 검사를 수행한다는 점에 주목할 필요가 있습니다. 따라서 와일드카드 동작이 필요한 사용자는 모든 질의나 원하는 다른 패턴에 대해 True를 반환하도록 __contains__를 구현한 사용자 정의 객체를 제공할 수 있습니다. 이 설계는 기본 메커니즘에 대해서는 단순하고 핵심에 집중한 명세를 유지하면서도, 고급 사용 사례에 필요한 유연성을 제공합니다. 이 PEP가 수락되면, 그러한 헬퍼 객체를 표준 라이브러리에 추가하는 방안은 향후 이슈에서 논의할 수 있습니다. 현재로서는 이 PEP의 범위를 벗어납니다.

with 블록 안에서 지연 임포트 금지하기

이 PEP의 이전 버전에서는 try 블록에 대한 제한과 마찬가지로 with 블록 안에서 lazy import 문을 금지하자고 제안했습니다. 특정 컨텍스트 관리자(contextlib.suppress(ImportError) 등)가 지연 임포트와 결합될 때 혼란스러운 방식으로 임포트 오류를 억제할 수 있다는 우려가 있었습니다.

그러나 with 문은 try/except 블록보다 훨씬 더 넓은 의미를 가지므로 이 제한은 기각되었습니다. try/except는 명시적으로 예외를 잡기 위한 것이지만, with 블록은 흔히 자원 관리, 일시적인 상태 변경, 스코프 지정에 사용되며, 이러한 맥락에서는 지연 임포트가 전혀 문제없이 동작합니다. lazy import 문법은 충분히 명시적이므로, 이를 with 블록 안에 작성하는 개발자는 의도적인 선택을 하는 것이며, 이는 Python의 “합의한 성인(consenting adults)” 철학과도 일치합니다. 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로 명시적으로 표시하든 전역 플래그를 통해 암묵적으로 지정하든 일관되게 동작하므로, 설명하기 쉽고 추론하기 쉬운 단순한 규칙이 만들어집니다.

dict 객체의 수정

지연 임포트에 대한 최초의 PEP(PEP 690)는 지연 임포트를 지원하기 위해 내부 dict 객체를 수정하는 데 크게 의존했습니다. 저희는 이 자료 구조가 고도로 튜닝되어 있고, 코드베이스 전반에서 매우 많이 사용되며, 성능에 매우 민감하다는 점을 인식하고 있습니다. 이 자료 구조의 중요성, 그리고 이 기능에 관심이 없을 수도 있는 사용자에게 지연 임포트의 구현이 드러나지 않도록 캡슐화하려는 바람 때문에, 저희는 다른 접근 방식에 투자하기로 결정했습니다.

딕셔너리는 Python의 근간이 되는 자료 구조입니다. 모든 객체의 속성은 dict에 저장되며, dict는 네임스페이스, 키워드 인자 등을 위해 런타임 전반에서 사용됩니다. 지연 임포트를 지원하기 위해 dict에 어떤 종류의 훅이나 특수 동작이든 추가하면 다음과 같은 결과를 낳습니다:

  1. 향후 JIT 컴파일을 포함한 중요한 인터프리터 최적화를 가로막습니다.
  2. 단순하고 빠르게 유지되어야 하는 자료 구조에 복잡성을 더합니다.
  3. 임포트 동작뿐만 아니라 Python의 모든 부분에 영향을 미칩니다.
  4. 관심사의 분리를 위반합니다 – 해시 테이블은 임포트 시스템에 대해 알아서는 안 됩니다.

핵심 추상화를 깔끔하게 유지한다는 이 원칙을 위반했던 과거의 결정들은 CPython 생태계에 상당한 고통을 초래했으며, 최적화를 어렵게 만들고 미묘한 버그를 유발했습니다.

__class__ 변경을 통한 지연 객체 변환

지연 임포트 객체를 완전히 교체하는 대신 내부 상태를 변경하여 최종 형태로 변환하는 대안적 구현 방식이 제안되었습니다. 이 방식에서는 실제 임포트가 완료된 후 지연 객체가 제자리에서 변환됩니다.

이 방식은 여러 가지 이유로 기각되었습니다:

  1. 이 기법은 모듈 객체에는 동작할 수도 있지만, from 문으로 임포트되는 임의의 객체에 대해서는 전혀 통하지 않습니다. 사용자가 lazy from foo import bar라고 작성하면, 객체 bar는 모듈뿐만 아니라 어떤 Python 객체(함수, 클래스, 상수 등)든 될 수 있습니다. 어떤 변환 방식이든 지연 프락시 객체가 대상 객체와 호환되는 메모리 레이아웃을 가지고 그 밖의 조건도 충족해야 하는데, 이는 모듈을 로드하기 전에는 알 수 없습니다. 이로 인해 lazy import x와 lazy from x import y가 완전히 다른 구현 전략을 필요로 하는 근본적인 비대칭이 생기며, 후자는 여전히 프락시 교체 메커니즘을 필요로 합니다.
  2. 모듈 객체의 경우에도 이 방식에는 근본적인 한계가 있습니다. 일부 구현은 표준 모듈 타입을 상속하거나 대체하는 사용자 정의 클래스를 sys.modules에 넣어 기존 항목을 대체합니다. 이러한 사용자 정의 모듈 클래스는 PyModuleObject와 다른 메모리 레이아웃과 크기를 가질 수 있습니다. 변환 방식은 이처럼 다양한 사용자 정의 모듈 구현 전반과 함께 동작할 수 없으므로, 생태계 전반에 취약성과 유지 보수 부담을 초래합니다.
  3. 변환 방식이 어떤 면에서는 더 단순해 보일 수 있지만, 이는 다소 주관적입니다. 이는 구현을 두 갈래로 나눕니다. 하나는 모듈을 위한 경로이고, 다른 하나는 모듈 객체가 아닌 객체를 위한 완전히 다른 경로입니다. 이것이 통합된 프락시 메커니즘보다 단순한지는 관점과 구현 세부 사항에 따라 다릅니다.
  4. 변환 전에 객체에 대한 참조를 보유하고 있던 코드는 변환 후에 다른 타입을 보게 됩니다. 이는 객체 타입을 검사하거나 타입 안정성에 의존하는 코드를 망가뜨릴 수 있으며, 특히 타입 포인터를 캐시하거나 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를 덧붙이는 방식을 논의했지만, 결국 이 접근 방식은 명확성이 떨어진다고 결론 내렸습니다. 예를 들어 하나의 문에서 여러 모듈을 임포트하는 경우, 지연 바인딩이 임포트된 모든 객체에 적용되는지 아니면 일부 항목에만 적용되는지가 불분명합니다.

명시적인 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의 데코레이터는 문(statement)이 아니라 호출 가능 객체(함수, 클래스, 메서드)를 감싸고 변환하도록 설계되었습니다. 임포트 문에 데코레이터를 허용하면 다른 많은 잠재적인 문 데코레이터(@cached, @traced, @deprecated 등)의 문을 열게 되어, 우리가 탐구하고 싶지 않은 방향으로 언어의 문법을 크게 확장하게 됩니다. 게다가 이는 그러한 데코레이터가 어디에서 오는지에 대한 문제를 제기합니다. 데코레이터는 임포트되거나 내장되어 있어야 하므로, 임포트 관련 데코레이터에 부트스트래핑 문제가 발생합니다. 이는 목적이 분명한 lazy import 문법보다 훨씬 더 추측에 기반하며 범용적입니다.

새로운 소프트 키워드 대신 컨텍스트 관리자 사용하기

예를 들어 컨텍스트 관리자 형태의, 하위 호환성을 갖춘 문법이 제안되었습니다:

with lazy_imports(...):
    import json

이렇게 하면 __lazy_modules__가 필요 없어지고, 라이브러리가 이전 Python 버전에서 기존 지연 임포트 구현 중 하나를 사용할 수 있게 됩니다. 하지만 그러한 효과를 가진 마법 같은 with 문을 추가하는 것은 Python과 with 문 전반에 대한 중대한 변경이 될 것이며, 이 제안의 지연 임포트 구현과 결합하기도 쉽지 않습니다. 구현을 변경하지 않고 기존 지연 임포터에 대한 표준 라이브러리 지원을 추가하는 것은 현재 상태와 다름없으며, 기존 해결책이 가진 성능 및 사용성 문제를 해결하지 못합니다.

globals()에서 프록시 dict 반환하기

globals() 호출 시 구체화하거나 지연 객체를 노출하는 방식의 대안은, 프록시를 통해 접근할 때 지연 객체를 자동으로 구체화하는 프록시 딕셔너리를 반환하는 것입니다. 이는 겉보기에 양쪽의 장점을 모두 취하는 것처럼 보입니다. globals()는 구체화 비용 없이 즉시 반환되지만, 결과를 통해 항목에 접근하면 지연 임포트가 자동으로 해결됩니다.

하지만 이 접근 방식은 실제로 globals()가 사용되는 방식과 근본적으로 호환되지 않습니다. 많은 표준 라이브러리 함수와 내장 함수는 globals()가 프록시가 아닌 실제 dict 객체를 반환할 것으로 기대합니다:

  • exec(code, globals())에는 실제 dict가 필요합니다.
  • eval(expr, globals())에는 실제 dict가 필요합니다.
  • type(globals()) is dict를 검사하는 함수는 제대로 동작하지 않게 됩니다.
  • .update() 같은 딕셔너리 메서드에는 특별한 처리가 필요하게 됩니다.
  • 접근할 때마다 간접 참조를 거치므로 성능이 저하됩니다.

프록시는 거의 모든 경우에 실제 dict와 구별할 수 없을 만큼 투명해야 하는데, 이를 올바르게 구현하기는 극도로 어렵습니다. 진짜 dict 동작에서 조금이라도 벗어나면 미묘한 버그의 원인이 됩니다.

__dict__ 또는 globals() 접근 시 자동으로 구체화하기

지연 임포트가 있을 때 globals()와 mod.__dict__가 어떻게 동작해야 하는지에 대해 세 가지 방안을 검토했습니다:

  1. globals() 또는 mod.__dict__를 호출하면 반환하기 전에 모든 지연 객체를 순회하며 해석합니다.
  2. globals() 또는 mod.__dict__를 호출하면 지연 객체가 들어 있는 상태 그대로 딕셔너리를 반환합니다(채택).
  3. 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를 만들어 가는 데 도움이 된 귀중한 피드백과 통찰을 제공해 주신 많은 분께 감사드립니다.

각주