[Final] PEP 338 - Executing modules as scripts
원문 링크: PEP 338 - Executing modules as scripts
상태: Final 유형: Standards Track 작성일: 16-Oct-2004
PEP 338 – 모듈을 스크립트로 실행하기
- 작성자: Alyssa Coghlan
- 상태: Final (최종)
- 유형: Standards Track
- 생성일: 2004년 10월 16일
- Python 버전: 2.5
- 요약: 이 PEP는
-m
명령줄 스위치 또는runpy.run_module(modulename)
함수를 통해 모든 Python 모듈을 스크립트로 실행하는 의미론을 정의합니다.
개요 (Abstract)
이 PEP는 -m
명령줄 스위치를 사용하거나 runpy.run_module(modulename)
을 호출하여 모든 Python 모듈을 스크립트처럼 실행하기 위한 의미론을 정의합니다. Python 2.4에서 구현된 -m
스위치는 매우 제한적이었으나, 이 PEP는 PEP 302의 임포트 훅(import hooks)을 활용하여 코드 객체에 접근할 수 있는 모든 모듈을 실행할 수 있도록 제안합니다.
도입 배경 (Rationale)
Python 2.4는 -m
명령줄 스위치를 도입하여 Python 모듈 이름 공간을 사용하여 모듈을 스크립트로 실행할 수 있도록 했습니다. 이는 pdb
나 profile
과 같은 표준 라이브러리 모듈을 실행하는 데 유용했습니다. 그러나 패키지 내부에 위치한 모듈(예: pychecker
의 pychecker.checker
모듈)을 실행하는 기능은 Python 2.4 구현에서 제외되었습니다. 이 기능의 구현이 훨씬 복잡하고 명확한 전략이 없었기 때문입니다.
python-dev
커뮤니티는 이 기능을 Python 2.5로 연기하고, PEP 프로세스를 통해 제대로 구현하는 것이 좋다고 판단했습니다. 또한, 기존 -m
버전은 zipimport
나 다른 형태의 대체 임포트 동작(예: frozen modules)을 지원하지 않는다는 문제도 지적되었습니다.
이 기능을 C로 작성하는 것보다 Python 모듈로 제공하는 것이 훨씬 쉽고, CPython 인터프리터에만 국한되지 않고 모든 Python 프로그램에서 사용할 수 있도록 합니다. CPython의 명령줄 스위치는 이 새로운 모듈을 활용하도록 재작성될 수 있습니다. profile
, pdb
와 같이 다른 스크립트를 실행하는 스크립트들도 새로운 모듈을 사용하여 실행할 스크립트를 -m
스타일로 식별하는 기능을 제공할 수 있습니다.
제안의 범위 (Scope of this proposal)
Python 2.4에서 -m
으로 찾은 모듈은 마치 파일 이름이 명령줄에 제공된 것처럼 실행됩니다. 이 PEP의 목표는 패키지 내부의 모듈이나 zipimport
와 같은 대체 임포트 메커니즘을 통해 접근하는 모듈에 대해서도 이 규칙이 최대한 적용되도록 하는 것입니다.
이 PEP는 Python 모듈을 스크립트로도 유용하게 만드는 관용구(idiom)를 변경하려는 것이 아님을 명시해야 합니다 (PEP 299 참조). 이 문제는 PEP 338이 다루는 특정 기능과 직교(orthogonal)하는 것으로 간주됩니다.
현재 동작 (Current Behaviour)
새로운 의미론을 설명하기 전에 Python 2.4의 기존 의미론을 살펴보는 것이 중요합니다. (현재 소스 코드와 명령줄 도움말에만 정의되어 있습니다).
- 명령줄에서
-m
이 사용되면, 옵션 목록은 즉시 종료됩니다 (-c
와 유사). - 인수는 최상위(top-level) Python 모듈의 이름으로 해석됩니다 (즉,
sys.path
에서 찾을 수 있는 모듈). - 모듈이 발견되고
PY_SOURCE
또는PY_COMPILED
유형인 경우, 명령줄은python <options> -m <module> <args>
에서python <options> <filename> <args>
로 효과적으로 재해석됩니다. 여기에는sys.argv[0]
을 올바르게 설정하는 것도 포함됩니다. (일부 스크립트는 이에 의존합니다 - Python 자체의regrtest.py
가 한 예입니다). - 모듈을 찾을 수 없거나 올바른 유형이 아니면 오류가 발생합니다.
제안된 의미론 (Proposed Semantics)
제안된 의미론은 매우 간단합니다. -m
을 사용하여 모듈을 실행하는 경우, PEP 302 임포트 메커니즘을 사용하여 모듈을 찾고 컴파일된 코드를 가져온 다음, 최상위 모듈에 대한 의미론에 따라 모듈을 실행합니다. 인터프리터는 새로운 표준 라이브러리 함수인 runpy.run_module
을 호출하여 이를 수행합니다.
이는 Python의 임포트 메커니즘이 패키지 내부의 모듈을 찾는 방식 때문에 필요합니다. 패키지는 초기화 중에 자체 __path__
변수를 수정할 수 있습니다. 또한, 경로는 *.pth
파일의 영향을 받을 수 있으며, 일부 패키지는 sys.metapath
에 사용자 정의 로더(custom loaders)를 설치합니다. 따라서 Python이 모듈을 안정적으로 찾을 수 있는 유일한 방법은 포함하는 패키지를 임포트하고 PEP 302 임포트 훅을 사용하여 Python 코드에 접근하는 것입니다.
실행될 모듈을 찾는 과정에서 포함하는 패키지를 임포트해야 할 수도 있습니다. 이러한 패키지 임포트가 실행될 모듈에 미치는 영향은 다음과 같습니다.
- 포함하는 패키지가
sys.modules
에 있게 됩니다. - 패키지 초기화의 외부 효과 (예: 설치된 임포트 훅, 로거,
atexit
핸들러 등).
참조 구현 (Reference Implementation)
참조 구현은 SourceForge에서 사용할 수 있으며 (), 라이브러리 참조 문서도 제공됩니다 (). 이 구현은 두 부분으로 구성됩니다.
- 제안된 표준 라이브러리 모듈
runpy
. -m
스위치 구현 코드의 수정. 모듈을 직접 실행하려 하지 않고 항상runpy.run_module
에 위임하도록 변경됩니다.
위임은 다음과 같은 형태를 가집니다:
runpy.run_module(sys.argv[0], run_name="__main__", alter_sys=True)
run_module
은 runpy
가 공개 API에서 노출하는 유일한 함수입니다.
run_module(mod_name[, init_globals][, run_name][, alter_sys])
지정된 모듈의 코드를 실행하고 결과 모듈 전역(globals) 딕셔너리를 반환합니다. 모듈의 코드는 먼저 표준 임포트 메커니즘을 사용하여 찾은 다음 (자세한 내용은 PEP 302 참조), 새로운 모듈 이름 공간에서 실행됩니다.
- 선택적 딕셔너리 인수
init_globals
는 코드가 실행되기 전에 전역 딕셔너리를 미리 채우는 데 사용될 수 있습니다. 제공된 딕셔너리는 수정되지 않습니다. __name__
,__file__
,__loader__
,__builtins__
와 같은 특수 전역 변수는 모듈 코드가 실행되기 전에 전역 딕셔너리에 설정됩니다.__name__
은run_name
인수가 제공되면 해당 값으로 설정되고, 그렇지 않으면 원래mod_name
인수로 설정됩니다.__loader__
는 모듈의 코드를 가져오는 데 사용된 PEP 302 모듈 로더로 설정됩니다.__file__
은 모듈 로더가 제공하는 이름으로 설정됩니다. 로더가 파일명 정보를 제공하지 않으면None
으로 설정됩니다.__builtins__
는__builtin__
모듈의 최상위 이름 공간에 대한 참조로 자동으로 초기화됩니다.
alter_sys
인수가 제공되고True
로 평가되면,sys.argv[0]
은__file__
값으로 업데이트되고,sys.modules[__name__]
은 실행 중인 모듈의 임시 모듈 객체로 업데이트됩니다. 이 함수가 반환되기 전에sys.argv[0]
과sys.modules[__name__]
은 원래 값으로 복원됩니다.
스크립트로 호출될 때 runpy
모듈은 첫 번째 인수로 제공된 모듈을 찾아 실행합니다. sys.argv[0]
(이는 runpy
모듈 자체를 참조합니다)을 삭제하여 sys.argv
를 조정한 다음, run_module(sys.argv[0], run_name="__main__", alter_sys=True)
를 호출합니다.
임포트 문과 메인 모듈 (Import Statements and the Main Module)
Python 2.5b1 출시 후, 이 PEP와 PEP 328 (명시적 상대 임포트) 사이에 놀라운 상호작용이 드러났습니다. 메인 모듈에서는 명시적 상대 임포트(explicit relative imports)가 작동하지 않습니다. 이는 상대 임포트가 __name__
에 의존하여 패키지 계층 구조에서 현재 모듈의 위치를 결정하기 때문입니다. 메인 모듈에서 __name__
값은 항상 '__main__'
이므로, 명시적 상대 임포트는 항상 실패합니다 (패키지 내부의 모듈에서만 작동하기 때문입니다).
메인 모듈이 직접 실행될 때 암시적 상대 임포트(implicit relative imports)가 작동하는 것처럼 보이지만, -m
을 사용하여 실행될 때는 실패하는 이유를 조사한 결과, 이러한 임포트는 실제로는 항상 절대 임포트(absolute imports)로 처리되는 것으로 나타났습니다. 직접 실행 방식 때문에 실행된 모듈을 포함하는 패키지가 sys.path
에 추가되어, 그 형제 모듈(sibling modules)이 실제로는 최상위 모듈로 임포트됩니다. 이는 직접 실행될 수 있는 모듈(예: 테스트 모듈 또는 유틸리티 스크립트)에서 암시적 상대 임포트를 사용하면 애플리케이션에 형제 모듈의 여러 복사본이 생길 수 있습니다.
Python 2.5 릴리스에서는 메인 모듈로 사용될 모든 모듈에서 항상 절대 임포트를 사용하는 것을 권장합니다. 여기서 -m
스위치가 이점을 제공합니다. -m
은 메인 모듈을 포함하는 디렉토리 대신 현재 디렉토리를 sys.path
에 삽입합니다. 이는 현재 디렉토리에 패키지의 최상위 디렉토리가 있는 한, -m
을 사용하여 패키지 내부의 모듈을 실행할 수 있음을 의미합니다. 패키지가 sys.path
의 다른 곳에 설치되어 있지 않아도 절대 임포트는 올바르게 작동합니다. 모듈이 직접 실행되고 형제 모듈을 검색하기 위해 절대 임포트를 사용하는 경우, 최상위 패키지 디렉토리가 sys.path
어딘가에 설치되어 있어야 합니다 (현재 디렉토리가 자동으로 추가되지 않기 때문입니다).
다음은 예시 파일 레이아웃입니다:
devel/
pkg/
__init__.py
moduleA.py
moduleB.py
test/
__init__.py
test_A.py
test_B.py
현재 디렉토리가 devel
이거나 devel
이 이미 sys.path
에 있고, 테스트 모듈이 절대 임포트(예: import pkg.moduleA
)를 사용하여 테스트 대상 모듈을 가져오는 한, PEP 338은 테스트를 다음과 같이 실행할 수 있도록 허용합니다:
python -m pkg.test.test_A
python -m pkg.test.test_B
메인 모듈이 -m
으로 실행될 때 상대 임포트가 지원되어야 하는지 여부는 Python 2.6에서 다시 논의될 문제입니다. 이를 허용하려면 Python의 임포트 의미론이나 모듈이 메인 모듈임을 나타내는 의미론에 변경이 필요하므로 서둘러 결정할 사안이 아닙니다.
해결된 문제 (Resolved Issues)
runpy
모듈 개발에 영향을 미친 몇 가지 핵심 설계 결정이 있었습니다.
- 특수 변수 설정:
__name__
,__file__
,__loader__
와 같은 특수 변수는 모듈이 실행되기 전에 모듈의 전역 이름 공간에 설정됩니다.run_module
은 이 값을 변경하므로, 제공된 딕셔너리를 직접 변이시키지 않습니다. (만약 그랬다면,globals()
를 이 함수에 전달했을 때 심각한 부작용이 발생할 수 있습니다.) - 정보 부족 시 처리: 때로는 특수 변수를 채우는 데 필요한 정보를 얻을 수 없을 때가 있습니다. 너무 똑똑하게 처리하려 하지 않고, 관련 정보를 결정할 수 없을 때는 이 변수들을 단순히
None
으로 설정합니다. alter_sys
인수에 대한 보호:alter_sys
인수에 대한 특별한 보호는 없습니다. 이로 인해 파일명 정보를 사용할 수 없는 경우sys.argv[0]
이None
으로 설정될 수 있습니다.- 스레드 문제:
alter_sys
가True
로 설정될 때 발생하는 잠재적 스레딩 문제를 피하기 위해 임포트 락(import lock)은 사용되지 않습니다. 대신, 스레드 코드는 이 플래그를 사용하지 않도록 권장됩니다.
대안 (Alternatives)
고려되었던 첫 번째 대안 구현은 패키지의 __path__
변수를 무시하고 메인 패키지 디렉토리만 살펴보는 것이었습니다. 이러한 동작을 가진 Python 스크립트는 execmodule
쿡북 레시피 () 토론에서 찾을 수 있습니다.
execmodule
쿡북 레시피 자체는 이 PEP의 초기 버전에서 제안된 메커니즘이었습니다 (PEP 작성자가 PEP 302를 읽기 전).
두 접근 방식 모두 -m
스위치의 주요 목표(명령줄에서 실행할 모듈을 찾기 위해 전체 Python 이름 공간을 사용할 수 있도록 하는 것)를 충족하지 못했기 때문에 거부되었습니다.
이 PEP의 초기 버전에는 exec
가 로컬 딕셔너리와 함수 객체에서 코드를 처리하는 방식에 대한 몇 가지 잘못된 가정이 포함되어 있었습니다. 이러한 잘못된 가정은 이제 제거된 불필요한 설계 복잡성을 초래했습니다. run_code
는 exec
의 모든 특이점을 공유합니다.
또한, 초기 버전의 PEP는 -m
스위치 업데이트를 구현하는 데 필요한 단일 run_module()
함수보다 더 광범위한 API를 노출했습니다. 단순성을 위해 이러한 추가 함수는 제안된 API에서 제외되었습니다.
SVN에서의 초기 구현 이후, 초기 애플리케이션 스크립트를 실행할 때 임포트 락을 유지하는 것이 올바르지 않다는 것이 분명해졌습니다 (예: python -m test.regrtest test_threadedimport
가 실패했습니다). 따라서 run_module
함수는 모듈을 실제로 검색하는 동안에만 임포트 락을 유지하며, alter_sys
가 설정되어 있어도 실행 전에 락을 해제합니다.
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments