PEP 810 – 명시적 지연 임포트
(pep-previews--4622.org.readthedocs.build)- Python은 모듈 수준에서 임포트를 모두 선언하는 것이 일반적인 관례임
- 하지만 프로그램 실행 시 불필요한 의존성 모듈까지 즉시 로드되어 시작 속도와 메모리 사용량 문제가 발생함
- 기존에는 함수 내 임포트 등으로 수동 지연 임포트를 많이 사용했으나, 유지보수와 의존성 관리가 어렵다는 단점이 존재함
- 이번 PEP 810은 local, explicit, controlled, granular한 새로운
lazy
키워드로 명시적 지연 임포트 문법을 도입함 - 이 기능 도입으로 실제 필요 시점에만 모듈을 로드하며, 시작 지연·메모리 낭비 개선 및 코드 구조 투명성을 동시에 실현함
Python 임포트의 현 상황과 문제점
- Python에서는 대체로 모듈 맨 위에 import문을 작성하는 관행이 널리 통용됨
- 이 방식은 중복을 줄이고, 임포트 의존성 구조를 한 눈에 파악하며, 한 번만 임포트하여 런타임 오버헤드를 최소화함
- 그러나, 프로그램 실행 시 첫 번째 모듈(main)이 로드되면 실제 사용하지 않는 많은 의존성 모듈까지 즉시 읽혀지는 연쇄 임포트가 일어나기 쉬움
- 특히 CLI 툴에서 전체 헬프만 호출해도 수십 개 모듈이 선 로드되는 등, 모든 서브커맨드마다 불필요한 오버헤드가 발생함
기존 대안과 문제점
- 임포트를 함수 내부로 옮기는 등 수동으로 임포트 시점을 늦추는 방식이 자주 사용됨
- 하지만 이 방식은 일관성·유지보수성 저하, 전체 의존성 파악 난이도 증가 등 단점이 큼
- 표준 라이브러리 분석 결과, 성능 민감 코드에서 이미 전체 임포트의 약 17%가 함수 또는 메서드 내부에서 임포트 지연 목적으로 사용되고 있음
- 임포트 지연 관련 도구로는
importlib.util.LazyLoader
, 서드파티lazy_loader
패키지 등이 있으나, 모든 케이스를 충족하지 못하거나 단일 표준이 부재함
PEP 810: 명시적 지연 임포트 도입
-
새로운
lazy
소프트 키워드를 도입 (특정 문맥에서만 의미를 갖고, 변수명 등으로도 쓸 수 있음) -
lazy
는 import문 앞에만 사용하며, 함수/클래스/with/try 등 영역이나 star import에는 쓸 수 없음 -
각 임포트문 단위로 명확히 구분해 사용 시점까지 모듈 로드를 지연시킴
lazy import 모듈명 lazy from 모듈명 import 이름
명시적 지연 임포트의 구현 방식 및 syntactic rule
-
문법 오류 케이스:
- 함수 내부, 클래스 내부, try/with, star import (
*
) 모두 불가
- 함수 내부, 클래스 내부, try/with, star import (
-
사용 예시:
import sys lazy import json print('json' in sys.modules) # False (아직 로드 전) result = json.dumps({"hello": "world"}) # 첫 사용 시 로드 print('json' in sys.modules) # True (지연 모듈 로드 완료)
-
모듈 단위로
__lazy_modules__
속성에 문자열 리스트로 lazy 대상 명시 가능__lazy_modules__ = ["json"] import json # lazy 로 처리됨
글로벌 플래그와 필터를 통한 동작 제어
-
글로벌 플래그 또는 필터 함수를 사용해 모듈 단위/전체에 lazy 적용 여부를 컨트롤 가능
-
필터 함수를 사용해 특정 모듈에만 eager import 예외 적용 가능
def my_filter(importer, name, fromlist): if name in {'problematic_module'}: return False # eager import return True # lazy import sys.set_lazy_imports_filter(my_filter)
런타임 동작 및 에러 처리
-
lazy import 사용 시 임포트 구문이 아닌 이름의 첫 접근 시점에 실제 임포트가 발생함
-
임포트에 실패할 경우, 예외 체인(traceback chaining) 으로 정의 위치와 발생 위치 모두를 명확하게 보여줌
lazy from json import dumsp # 오타 result = dumsp({"key": "value"}) # 실제 접근 시점에서 ImportError 발생
메모리 및 성능 이점
- 지연된 모듈은 sys.lazy_modules 집합에만 표시되고, 실제 사용 전 sys.modules에 등록되지 않음
- 사용 후에는 정상 모듈 객체로 대체되고, 추가적인 성능 페널티 없이 사용 가능
- 실제 워크로드 환경에서는 시작 지연 50~70% 감소, 메모리 30~40% 절감 효과가 나타남
동작 방식 요약
- lazy object의 처음 접근 시 reification(실제 임포트 및 대체)이 발생함
- 외부 코드에서 모듈의
__dict__
접근 시에는 모든 lazy object가 강제 로드됨 (reification) -
globals()
로 딕셔너리 추출 시에는 lazy proxy가 유지되어 직접 접근 필요
타입 어노테이션 및 TYPE_CHECKING 최적화
-
lazy from 모듈 import 이름
으로 타입만 사용하는 임포트에 런타임 비용 ZERO 보장 - 기존의
from typing import TYPE_CHECKING
조건문을 대체해 코드가 더 간결·명확해짐
기존 PEP 690과의 차이 및 구현상의 특징
- PEP 810은 명시적, 개별 임포트 단위, 간단한 프록시 객체 기반 opt-in 구조
- 반면 PEP 690은 global, 암시적 lazy import 구조였음
주의사항 및 모듈간 상호작용
- star import (
*
)는 lazy로 지원하지 않음 (항상 eager) - 커스텀 import hook, loader는 reification 타이밍에 그대로 작동
- 멀티스레드 환경에서도 thread-safe하게 한 번만 임포트 및 안전한 바인딩 보장
- 동일 모듈의 lazy·eager 동시 사용 시 eager쪽이 항상 우선됨
코드 적용 및 마이그레이션 가이드
- 기존 코드에서 적용 시 프로파일링으로 필요한 임포트만 lazy로 변환, 점진적 적용 권장
-
__lazy_modules__
활용시 Python 3.15 미만 버전에서도 호환
기타 주요 질문과 답변 포인트
- 임포트 타임 부작용 (예: 등록 패턴 등)은 첫 접근까지 지연됨. side effect가 필수라면 명시적 초기화 함수 패턴 추천
- circular import(순환 임포트) 문제는 lazy import로 완전 해결 불가 (access가 늦춰져야만 완화 가능)
- 핫패스 성능은 first use 이후 lazy 체크가 완전히 사라져 자동 최적화됨 (바이트코드 adaptive specialization)
-
sys.modules
에는 reification(첫 사용) 후에만 실제 모듈이 등록됨 -
importlib.util.LazyLoader
와 달리 별도 설정 불필요, 성능 유지, standard syntax 명확성
결론
- PEP 810은 Python import문에
lazy
키워드를 추가하여, 서브커맨드 CLI, 대형 애플리케이션, 타입 어노테이션 등 다양한 영역에서 불필요 모듈 로딩으로 인한 성능 문제를 간결하고 예측 가능하게 최적화할 수 있게 해줌 - 새 키워드는 도입 시점과 대상을 세밀하게 지정할 수 있어 실서비스에서 점진적 도입 및 성능 튜닝에 적합함
- Python import 체계의 실질적 진화로, 가시성, 유지보수성, 성능 세 가지 요구를 동시에 충족함
Hacker News 의견
-
내 llm.datasette.io CLI 툴은 플러그인을 지원함, 그런데 "llm --help" 같은 커맨드에서도 너무 느린 시작 시간을 겪는다는 불만이 많았음, 확인해보니 인기 플러그인들이 기본적으로 pytorch 같은 무거운 패키지를 임포트해서 전체 시작이 막히는 문제였음, 그래서 플러그인 작성자 문서에 함수 내부에서 필요한 경우에만 의존성을 임포트하라고 안내함(관련 문서 링크), 하지만 이런 문제를 Python 언어 차원에서 지원하면 훨씬 좋겠다는 생각임
-
오늘 당장이라도 도구에서 이 기능 구현할 수 있음 (설명 링크), 단 이 방식은 프로세스 전체에 글로벌로 적용되기 때문에 numpy를 느리게 임포트하면 하위 모듈 임포트도 모두 느려짐, 결국 numpy의 전체가 필요 없으면 아예 임포트되지 않을 수 있지만, 필요한 시점에 부분적으로 모듈을 임포트하는 지연 현상이 런타임 동안 예측할 수 없게 분산될 수 있음, 추가 실험 결과
import foo.bar.baz
처럼 임포트하면 foo와 foo.bar는 여전히 즉시 로드되고 foo.bar.baz만 지연됨, 아마 PEP에서 "mostly"라고 표현한 일부 이유인 듯, 내 구현을 더 개선한다면 이걸 해결할 수도 있을 것 같음 -
커맨드라인을 먼저 파싱해서 "--help" 같은 옵션은 임포트 없이 처리하는 방법을 추천함, 정말 필요할 때만 임포트를 실행하고, 또는 간단히 말해 쉬운 커맨드 옵션이 처리되고 아직 할 일이 남아 있을 때만 임포트하도록 설계하는 방식도 괜찮음
-
-
Lazy import 제안은 과거에도 있었고, 가장 최근에는 2022년에 거절당함 (관련 토론 링크), 내 기억으로는 lazy import가 Meta의 CPython 변형인 Cinder에는 이미 들어가 있으며, 이번 PEP도 Cinder 작업하던 사람들이 주도함, 논의의 초점은 "opt-in이냐 opt-out이냐?" "적용 범위는 어디까지냐?" "CPython 빌드 플래그로 넣어야 하냐?" 등이었음, 결국 Steering Council이 임포트 동작이 둘로 갈라지는 복잡성 때문에 거절했다고 함, 이번 제안이 꼭 통과되길 바람, 이 기능을 정말 쓰고 싶음
-
특히 opt-in 방식이고, 세밀한 레벨별 적용과 글로벌 종료 스위치까지 포함된 점이 마음에 듦, 여러 제약 사항 내에서 매우 잘 구성된 명세임
-
나도 이 제안이 통과되길 바라지만, 낙관적이진 않음, 이건 수많은 코드를 깨뜨리고 예기치 못한 문제도 쏟아낼 것임, import 구문은 근본적으로 부작용이 있는데 그 적용 시점이 달라지면 오랜 시간 원인 모를 버그로 고생하게 될 것임, 이건 불안 조장이 아니라 실제 이유가 있는 걱정임, lazy import가 Meta에만 들어갔던 것에도 이유가 있음—Meta만큼 자원이 풍부해야 다룰 수 있을 만한 일이기 때문임, 많은 사람들이 "pandas, numpy, 또는 꼬인 내 weird module이 너무 느리니 빨라지면 좋겠다" 만을 보고 있는데, 실제로 Python의 import 시스템이 어떻게 돌아가는지 조차 아는 사람이 드물다고 생각함, 심지어 lazy import 구현 방법도 모르면서 찬성하는 의견이 많음, PEP 690을 보면 단점이 여럿 있음—예시로, 데코레이터를 써서 함수들을 중앙 registry에 추가하는 코드가 깨짐, 대표적으로 Dash 라이브러리는 자바스크립트 기반 인터페이스와 Python 콜백을 import 시점에 데코레이터로 엮어서, 임포트가 lazy가 되면 아예 이런 프론트엔드가 죽어버릴 것임, 수많은 사용자를 가진 서비스도 즉시 망가질 수 있음, “opt-in이니까 안 맞으면 lazy import를 끄면 된다” 라고 하는데, 임포트가 전이(transitive)라면? 프론트엔드가 완전히 초기화된 후 중요한 프로세스를 시작해야 하는 경우라면? 여러 사람의 코드와 라이브러리가 얽힌 생태계에서 어떤 영향이 있을지 누가 알 수 있겠음? 타입힌트와 달리 런타임 동작에 실질적 영향을 주는 변경임, 임포트 구문은 모든 실질적 Python 코드에 다 들어가므로, lazy가 도입되면 근본적으로 실행 방식이 바뀌는 것임, 이 외에도 PEP이 언급한 이상한 케이스들이 더 있음, 생각보다 훨씬 힘든 문제임
-
import torch==2.6.0+cu124
,import numpy>=1.2.6
처럼 버전 명시 임포트와, 동일 파이썬 환경에 여러 버전 패키지를 동시에 설치/임포트할 수 있으면 정말 좋겠음, 이제는 conda/virtualenv/docker/bazel 지옥 좀 끝냈으면 함
-
-
그다지 싫지는 않지만 엄청 반기지도 않음, 이대로라면 거의 모든 import 앞에
lazy
를 붙이게 될 것 같은데, 실제로 eager하게 임포트해야 하는 몇몇 경우만 빼고 나머지는 죄다 lazy를 붙이니 코드가 지저분해짐, 그리고 이게 기본 동작으로 바뀔 계획도 없으니 이 번잡스러움은 영원히 남을 것임, 난 차라리 모듈 쪽에서 lazy loading을 opt-in하도록 선언하고, import 문법엔 변화가 없는 시스템이 더 나았다고 생각함, 그러면 대형 라이브러리만 laziness 신경 쓰면 됨, 물론 그렇게 하면 인터프리터가 임포트 시점에 파일 시스템을 뒤져야 하고 다른 단점도 있겠지만 말임-
모두가 별다른 문제 없이 lazy import를 많이 쓴다면 lazy가 기본값이었어야 하고, 오히려 <i>eager</i>가 선택적 키워드였어야 한다는 뜻임, 이런 패러다임 변경은 파이썬에서 처음 있는 게 아님, v2에서 eagerly list를 만들던 여러 구문이 v3에선 generator로 바뀌었지만 별문제도 없었음
-
커맨드라인 플래그로 파이썬 전체 모듈 임포트를 lazy로 만드는 옵션이 있다면 무조건 쓸 의향임, 실제로 스크립트나 정말 단순한 코드 말고는 module load할 때 side effect 생기는 건 정말 피해야 하는 패턴임
-
모듈 쪽에서 lazy loading 여부를 결정하는 건 맞지 않다고 봄, 호출자만이 lazy load가 필요한지 알 수 있으니 import하는 코드 쪽에서 옵션을 주는 게 타당함, 어떤 모듈이든 lazy load가 가능하고, side effect가 있어도 호출자는 그것까지 지연하고 싶을 수 있음
-
pyproject.toml에 regex로 lazy loading 옵션을 명시할 수 있었으면 함
-
과거 type hint, walrus, asyncio, dataclasses 등등 새로운 기능이 나올 때마다 사람들이 유사한 우려를 했지만 실제로 그렇게 많은 사람들이 일괄로 쓰거나 기존 패턴을 모두 바꾸진 않았음, 많은 사용자는 여전히 modernized 된 python 2.4 수준 기능만 쓰고 있고, 그렇게 해도 충분히 생산적임, 20년째 잘 굴러 왔으니, 큰 문제는 없을 것 같음
-
-
관심 있다면 context manager 형태로 매우 편리하게 lazy import를 구현한 lazyimp를 소개함, 보통 import 구문을 with 블록으로 감싸기만 하면 돼서 기존 툴과도 잘 통하고, 디버깅이 필요하면 쉽게 eager import로 전환 가능함, cext로 frame의 f_builtins를 바꿔 importlib hook보다 더 강력하게 만듦, 완벽하진 않지만 쓰레드 세이프 버전과 글로벌 핸들러 버전도 있음, 처음엔 조심스러웠지만 지금은 코드베이스 거의 대부분을 이걸로 옮겼고, 실제 문제는 (모듈별 등록 처리를 안 챙긴 것 빼면) 전혀 없었고 속도 체감이 엄청 커서 만족함
-
파이썬 린터들이 임포트를 파일 상단에 두라고 강제하는 문제는 정말 불편함, obvious한 lazy import 구현법을 쓸 때마다 린트 에러가 남, 이 문제는 단순한 성능 이슈 그 이상임, 예를 들어 플랫폼 특화 라이브러리가 필요할 때, 그 플랫폼에서만 import하고 싶어도 상단 import를 강제하면 아예 import가 실패할 수도 있음
-
그럴 땐 그냥 린터를 고치는 수밖에 없다고 생각함
-
대부분의 린터는
#noqa E402
같은 주석으로 무시할 수 있음
-
-
LazyLoader 클래스를 통해 어느 정도 자동 lazy import가 가능하다는 얘기가 있음, 다만 Python import 내부를 활용하는 방식이 워낙 명확하지 않아 Stack Overflow에서도 설명이 그다지 보기 좋지 않음 (관련 Q&A), 그래서 프로그래머가 명시적 문법 없이 모든 import를 lazy로 만드는 증명 개념 코드를 직접 구현해 봄
import sys
import threading # 파이썬 3.13에선 필요, REPL에서만이라도
from importlib.util import LazyLoader # 이건 반드시 즉시 import해야 함!
class LazyPathFinder(sys.meta_path[-1]): # _frozen_importlib_external.PathFinder 상속
@classmethod
def find_spec(cls, fullname, path=None, target=None):
base = super().find_spec(fullname, path, target)
base.loader = LazyLoader(base.loader)
return base
sys.meta_path[-1] = LazyPathFinder
이렇게 하면 meta path finder를 감싸는 래퍼로 교체해서, loader를 LazyLoader로 대체함, import가 실행될 때 실제로 모듈 이름이 <class 'importlib.util._LazyModule'>
로 바인딩되고, 속성을 액세스할 때 진짜 모듈이 로드됨, 실험 코드:
import this # 아무런 결과도 안 나옴
print(type(this)) # <class 'importlib.util._LazyModule'>
rot13 = this.s # Zen이 출력됨, 이 시점에 모듈 로딩
print(type(this)) # <class 'module'>
다만 PEP에서 "mostly"란 표현의 정확한 의미는 모르겠음
-
lazy 임포트에는 스레드 안전성 위험이 과소평가된 것 같음, 임포트가 언제, 어떤 스레드, 어떤 락을 잡고 실행될지 전혀 예측할 수 없고, importer 락 외에는 장담할 수 없음, 예전엔 모듈 임포트 시점에 위험한 코드가 실행되더라도 대부분 단일 스레드 초기화 과정에서만 일어나서 큰 문제는 없었음, lazy로 바뀌면 진짜 예측 불가한 방식(Heisenbug)으로 에러가 튀어나옴, 함수 레벨 import도 이런 문제 가능성은 있지만 적어도 명시적 코드의 맨 처음에 실행된다는 예측성이라도 있음
-
좋은 기능으로 느껴짐, 설명도 쉽고, 실제 사용사례와 (글로벌용, 간단한 키워드 방식) 범위도 적당함, 마음에 듦
-
최근 나온 PEP 중에서는 사용자 입장에선 가장 깔끔하다고 느낌, 이 전통적인 Syntax bikeshedding(문법 논쟁) 과정을 거친 뒤에 실제 결과가 기대됨
-
실무와 edge case 사례 검증, 적절한 절충, 과하지 않은 방식, 여러 번 다듬은 점 등 꼼꼼하게 준비한 PEP이라 생각함, 특히 전 세계 각양각색 커뮤니티를 가진 대형 언어의 핵심(bone) 시스템에 손대는 일이라 매우 위험할 수 있는데, 그 어려움을 고려하면 특히 인상 깊음
-
왜 PEP-690이 거부됐는지 충분히 배웠기를 바람, 우리 코드베이스에서도 이런 기능을 직접 구현하려 시도했지만, 쓸만한 수준으로 잘 동작한 적이 없었음
-
-
lazy import는 길게 동작하는 서비스에서 예기치 못한 런타임 에러를 만들기 쉽다는 점이 위험함, 빠른 스타트업이라는 장점처럼 보이지만, 코드 실행이 중간에 임포트 실패로 멈춰버릴 가능성을 떠안는 tradeoff임, 추가로 임포트될 대상이 프로그램 시작 시점엔 어떤 것인지 장담하지 못하게 되는 엣지 케이스도 생길 수 있음
-
그래도 이건 반드시 해결해야 하는 진짜 문제임, 스타트업 속도만의 문제가 아니라 파이썬 스타트업이 큰 의존성이 들어가면 말도 안 되게 느려짐, 대형 프로젝트들은 모든 사용자가 쓰지도 않을 무거운 라이브러리를 모두 번들링할 수도 없어서, 개발자들은 이미 더 해괴한 우회책을 쓰고 있고, 그 역시 말도 안 되는 문제를 더하는 꼴임, 함수-level import를 중복해서 감추거나 숨겨야 하는 불편함만 해결돼도 큰 진전임, 그리고 이건 어디까지나 optional language feature로 제안되고 있음
-
자동화된 테스트로 충분히 리스크 완화 가능함, 빠른 스타트업과 맞바꿀 만한 값어치가 있음, 스타트업 타임은 결코 "겉모습만"의 문제가 아님, 난 Django 모놀리식에서 이런 문제로, 단 몇 개의 무거운 라이브러리 때문에 모든 management command, test, 컨테이너 reload마다 10~15초씩 대기하는 상황을 겪었음, lazy import로 defer했더니 엄청난 차이가 났음
-
-
우리는 명시적 최상단 import를 선호하는 편인데, 그 이유는 프로그램 시작 시점에 바로 의존성 문제를 드러내게 하려고 해서임, lazy import를 쓰면 특정 코드 경로가 실행될 때(아마도 몇 시간, 며칠 후) 문제를 발견하는 불편함이 생김
- 반대로, 모든 import가 자동으로 deferred(지연)된다면 짧은 작업에서 pip 실행 속도가 즉각 빨라질 것임
$ time pip install --disable-pip-version-check
ERROR: You must give at least one requirement to install (see "pip help install")
real 0m0.399s
user 0m0.360s
sys 0m0.041s
대부분의 시간은 실제론 쓰지도 않는(예: Requests 연관 모듈만 거의 100개) vendor 모듈을 임포트와 언로드하는 데 들어감, 진단해보니 총 500개 이상 모듈이 불필요하게 임포트되고 있음
-
코드 생성기도 상단 import 대신 함수 안에 local import를 집어넣는 코드가 많아진 원인을 모르겠음, 그 패턴을 권장하고 싶지 않음, 이유는 모듈의 의존성 파악이 어려워지고, 나중에 사이클릭 디펜던시가 생길 위험이 커지기 때문임
-
PEP을 아직 다 읽진 않았지만 혹시 커맨드라인 플래그나 외부 툴로 dependency validation이 가능하면 좋겠다는 생각임, type hint에 대응하는 도구처럼 말임
-
"우리는"이 정확히 누구를 의미하는지 궁금함
-
이건 테스트로 커버해야 하는 문제 아닐까 싶은 생각임