본문으로 건너뛰기

[CPython] PEP 810 -- CPython에 명시적 Lazy Import 구현

PR 링크: python/cpython#142351 상태: Merged | 변경: +5126 / -197

들어가며

대규모 Python 애플리케이션은 깊은 의존성 트리 때문에 시작 시간이 수 초에 달하는 문제가 있다. 실행 중 실제로 사용하지 않는 모듈까지 모두 로드하기 때문이다. 기존 해결책은 함수 내부로 import를 옮기거나 importlib를 직접 사용하는 것이었는데, 이는 코드 가독성을 해치고 유지보수 부담을 늘린다. PEP 810은 lazy라는 새 soft keyword를 도입해 import 시점을 최초 사용 시점까지 지연시키는 공식 메커니즘을 제공한다.

핵심 코드 분석

Grammar 변경: lazy soft keyword 추가

Before:

# Grammar/python.gram
import_name[stmt_ty]: 'import' a=dotted_as_names { _PyAST_Import(a, EXTRA) }
import_from[stmt_ty]:
    | 'from' a=('.' | '...')* b=dotted_name 'import' c=import_from_targets {
        _PyPegen_checked_future_import(p, b->v.Name.id, c, _PyPegen_seq_count_dots(a), EXTRA) }

After:

# Grammar/python.gram
import_name[stmt_ty]:
    | lazy="lazy"? 'import' a=dotted_as_names { _PyAST_Import(a, lazy ? 1 : 0, EXTRA) }
import_from[stmt_ty]:
    | lazy="lazy"? 'from' a=('.' | '...')* b=dotted_name 'import' c=import_from_targets {
        _PyPegen_checked_future_import(p, b->v.Name.id, c, _PyPegen_seq_count_dots(a), lazy, EXTRA) }

Grammar 레벨에서 lazy를 optional prefix로 추가했다. lazy는 soft keyword이므로 기존 변수명 lazy를 사용하는 코드에 영향을 주지 않는다. parser가 import 또는 from 앞에 lazy가 올 때만 키워드로 인식한다.

AST 구조 변경: is_lazy 필드

Before:

struct {
    asdl_alias_seq *names;
} Import;

struct {
    identifier module;
    asdl_alias_seq *names;
    int level;
} ImportFrom;

After:

struct {
    asdl_alias_seq *names;
    int is_lazy;
} Import;

struct {
    identifier module;
    asdl_alias_seq *names;
    int level;
    int is_lazy;
} ImportFrom;

AST의 ImportImportFrom 노드 모두에 is_lazy 필드가 추가되었다. Compiler는 이 플래그를 확인해 lazy import 전용 opcode 경로를 생성한다.

LazyImport Proxy 객체

// Include/internal/pycore_lazyimportobject.h
typedef struct {
    PyObject_HEAD
    PyObject *lz_builtins;
    PyObject *lz_from;
    PyObject *lz_attr;
    PyCodeObject *lz_code;
    int lz_instr_offset;
} PyLazyImportObject;

lazy import가 실행되면 실제 모듈 대신 PyLazyImportObject proxy가 생성되어 이름에 바인딩된다. 이 proxy는 최초 attribute 접근 시 실제 모듈을 로드하고 자신을 대체한다. lz_codelz_instr_offset을 저장해서 에러 발생 시 원래 import 위치를 traceback에 포함할 수 있다.

글로벌 모드 제어: sys API와 환경변수

# 사용 예시
import sys

def myapp_filter(importing, imported, fromlist):
    return imported.startswith("myapp.")

sys.set_lazy_imports_filter(myapp_filter)
sys.set_lazy_imports("all")

import myapp.slow_module  # lazy (filter 통과)
import json               # eager (filter 거부)

-X lazy_imports=all 옵션이나 PYTHON_LAZY_IMPORTS 환경변수로 소스 코드 수정 없이 전역적으로 lazy import를 활성화할 수 있고, filter 함수로 모듈별 세밀한 제어가 가능하다.

왜 이게 좋은가

  1. 시작 시간 단축: 사용하지 않는 모듈의 로드를 완전히 건너뛴다. 의존성이 깊은 CLI 도구나 웹 프레임워크에서 체감 효과가 크다.
  2. 코드 구조 유지: import문을 파일 상단에 유지하면서도 지연 로딩의 이점을 얻는다. 함수 내부 import 패턴이 불필요해진다.
  3. 점진적 도입: soft keyword이므로 기존 코드와 완전히 호환된다. lazy라는 변수명을 쓰는 기존 코드도 영향받지 않는다.
  4. Cycle 감지: ImportCycleError라는 새 예외를 추가해 lazy import가 자기 자신을 재귀적으로 import하려 할 때 명확한 에러를 제공한다.

정리

PEP 810은 Python의 import 시스템에 lazy soft keyword를 도입해 명시적 lazy import를 가능하게 한다. Grammar, AST, Compiler, Interpreter(opcode) 전 레이어에 걸친 대규모 변경이며, proxy 객체 기반의 투명한 지연 로딩 메커니즘을 구현했다. 대규모 애플리케이션의 시작 시간 최적화에 실질적인 해법을 제공하면서도 기존 코드와의 완전한 하위 호환성을 유지한 설계가 인상적이다.

참고 자료

알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.

댓글

관련 포스트

PR Analysis 의 다른글