PEP 499 – python -m foo는 sys.modules에 'foo'도 바인딩해야 합니다
- Author:
- Cameron Simpson <cs at cskk.id.au>, Chris Angelico <rosuav at gmail.com>, Joseph Jevnik <joejev at gmail.com>
- BDFL-Delegate:
- Alyssa Coghlan
- Status:
- Deferred
- Type:
- Standards Track
- Created:
- 07-Aug-2015
- Python-Version:
- 3.10
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
PEP 보류
이 PEP의 구현은 현재로서는 2020년 4월의 Python 3.9 기능 동결까지 준비되지 않을 것으로 예상되므로, 12개월 연기하여 Python 3.10으로 미루었습니다.
초록
다음과 같이 Python 명령줄에서 모듈을 메인 프로그램으로 사용할 때:
python -m module.name …
그 모듈을 프로그램 안에서 다시 임포트하면 실수로 서로 독립적인 모듈 인스턴스 두 개가 생기기 쉽습니다. 이 PEP는 이 문제를 해결하는 방법을 제안합니다.
Python의 -m 옵션으로 모듈을 실행하면 모듈은 sys.modules['__main__']에 바인딩되고 .__name__ 속성은 '__main__'으로 설정됩니다. 이 덕분에 많은 모듈의 맨 아래에 있는 다음과 같은 표준 “메인 프로그램” 상용구 코드를 사용할 수 있습니다:
if __name__ == '__main__':
sys.exit(main(sys.argv))
그러나 위와 같은 명령줄 호출을 사용하면, 모듈이 실제로 공식 이름인 module.name으로 임포트되었고, 따라서 프로그램이 그 이름을 다시 임포트하면 같은 모듈 인스턴스를 얻을 것이라고 추론하는 것이 자연스럽습니다.
실제로는 모듈이 '__main__'으로만 임포트되었습니다. 다시 임포트하면 별개의 모듈 인스턴스를 얻게 되며, 이는 혼란스러운 버그로 이어질 수 있습니다. 이러한 버그는 모두 모듈 전역 객체의 인스턴스가 각 모듈에 하나씩, 두 개 존재한다는 데서 비롯됩니다.
예는 다음과 같습니다
- 모듈 수준 자료 구조
- 일부 모듈은 캐시나 레지스트리 같은 기능을 모듈 수준 전역 변수로 제공하며, 이 변수는 보통 비공개입니다. 모듈의 두 번째 인스턴스는 두 번째 자료 구조를 만듭니다. 그 자료 구조가
re모듈에 있는 것과 같은 캐시라면 캐시가 두 개 존재하게 되어 메모리가 낭비됩니다. 그 자료 구조가 값과 핸들러의 매핑 같은 공유 레지스트리라면, 한 레지스트리에 핸들러를 등록해 놓고 그 핸들러를 알지 못하는 다른 레지스트리를 통해 사용하려 하는 일이 생길 수 있습니다. - 센티널
- 모듈이 제공하는 센티널 값을 검사하는 표준 방법은
is를 사용한 동일성 비교입니다. 이 방법을 쓰면 동등성 비교처럼 신뢰할 수 없는 “비슷해 보이는지” 비교를 피할 수 있기 때문입니다. 그런 비교는 두 값을 잘못해서 “같다”고 판단할 수도 있고(예를 들어 둘 다 0에 해당하는 값일 때), 객체끼리 호환되지 않을 때TypeError를 일으킬 수도 있습니다. 모듈의 인스턴스가 두 개이면 센티널 인스턴스도 두 개가 되며,is로는 그중 하나만 인식됩니다. - 클래스
- 두 모듈이 있으면 제공되는 모든 클래스의 클래스 정의가 중복됩니다. 이러한 클래스와 그 서브클래스를 인식하는 데 의존하는 모든 연산은, 기준 클래스를 (두 모듈 중 어느 쪽에서) 얻었는지와 비교할 클래스나 인스턴스를 어디에서 얻었는지에 따라 실패하기 쉽습니다. 이는
isinstance,issubclass뿐만 아니라try/except구문에도 영향을 줍니다.
제안
이 상황을 해결하는 데 필요한 것은 -m 옵션의 구현 방식을 간단히 바꾸는 것뿐이라고 제안합니다. 즉, 모듈 객체를 sys.modules['__main__']에 바인딩할 뿐만 아니라 sys.modules['module.name']에도 바인딩하는 것입니다.
Alyssa (Nick) Coghlan은 runpy 모듈의 _run_module_as_main 함수를 다음과 같이 수정하기만 하면 된다고 제안했습니다:
main_globals = sys.modules["__main__"].__dict__
대신 다음과 같이 바꾸는 것입니다:
main_module = sys.modules["__main__"]
sys.modules[mod_spec.name] = main_module
main_globals = main_module.__dict__
Joseph Jevnik은 패키지인 모듈이 이미 이 제안과 매우 비슷한 방식으로 동작한다고 지적했습니다. __init__.py 파일은 모듈의 정식 이름에 바인딩되고, __main__.py 파일은 “__main__”에 바인딩됩니다. 그렇기 때문에 이중 임포트 문제가 발생하지 않습니다. 따라서 이 PEP는 패키지가 아닌 단순 모듈에만 영향을 주도록 제안합니다.
고려 사항 및 전제 조건
모듈 피클링
Alyssa는 다음을 제안하는 issue 19702를 언급했습니다(이슈에서 인용):
- runpy는 __main__이 임포트 시스템을 통해 실행될 때 sys.modules에 __spec__.name으로도 별칭이 지정되도록 보장합니다
- __main__.__spec__이 설정되어 있으면, pickle은 __main__에 정의된 클래스, 함수, 메서드를 피클링할 때 __name__ 대신 __spec__.name을 사용합니다
- 부모 프로세스에서 __main__.__spec__이 설정되어 있으면 자식 프로세스에서 __mp_main__을 생성하지 않도록 multiprocessing을 적절히 갱신합니다
위의 첫 번째 항목이 이 PEP의 구체적인 제안에 해당합니다.
일반 모듈의 __name__은 더 이상 정식 이름이 아닙니다
Chris Angelico는 __name__이 “import”에 전달한 이름과 다른 모듈을 임포트하는 일이 가능해진다고 지적합니다. 이제 “__main__”이 “module.name”에 존재하므로, 이후의 import module.name은 해당 모듈이 이미 존재한다는 것을 발견하기 때문입니다. 따라서 일부 일반 임포트에서는 __name__이 더 이상 정식 이름이 아닙니다.
다음은 몇 가지 반대 인자입니다:
- PEP 451부터 모듈의 정식 이름은
__spec__.name에 저장됩니다. __name__이 정규 이름인지 실제로 신경 써야 하는 코드는 거의 없으며, 그런 코드가 있다면__spec__.name을 참조하고, 해당되는 경우 이전 Python을 위해__name__으로 대체하도록 수정하는 것이 마땅하다고 볼 수 있습니다. 이는 이 PEP가 승인되지 않더라도 마찬가지입니다.- 이 PEP가 승인되면, 모듈을 정식 이름으로 인트로스펙션하면서
__name__으로부터 추론하여 “이것이 메인 프로그램이었는가?”를 물어볼 수 있게 됩니다. 이는 이전에는 불가능했습니다.
눈에 띄는 반례는 __name__이 “__main__”일 것으로 기대하는 표준적인 “내가 메인 프로그램인가?” 상용구 코드입니다. 이 PEP는 그 의미를 명시적으로 보존합니다.
참조 구현
BPO 36375는 이 PEP의 참조 구현에 대한 이슈 트래커 항목이며, 현재 초안 PR은 GitHub에서 확인할 수 있습니다.
미해결 질문
이 제안은 하위 호환성 문제를 일부 일으키며, 이 문제를 충분히 이해한 다음 폐지 절차를 설계하거나 명확한 이식 지침을 제공해야 합니다.
피클 호환성
pickle 모듈을 변경하지 않으면, 이전에는 (이중 임포트 덕분에) 올바른 모듈 이름으로 기록되던 피클이 대신 __main__을 모듈 이름으로 하여 기록되기 시작할 수 있으며, 그 결과 다른 프로젝트에서 올바르게 로드되지 못할 수 있습니다.
확인해야 할 시나리오는 다음과 같습니다.
python script.py로 기록하고python -m script로 읽기python -m script로 기록하고python script.py로 읽기python -m script로 기록하고python some_other_app.py로 읽기old_python -m script로 기록하고new_python -m script로 읽기new_python -m script쓰기,old_python -m script읽기
__main__을 특별 취급하는 프로젝트
회귀 테스트 스위트를 통과시키기 위해, 현재 참조 구현은 pdb가 자신의 전역 네임스페이스를 파괴하지 않도록 pdb를 패치해야 했습니다.
이는 일부 스크립트가 직접 실행과 임포트에서 서로 다른 네임스페이스가 주어진다는 점에 의존하는, 더 넓은 호환성 문제가 있을 수 있음을 시사합니다(패키지 실행 시 __main__ 하위 모듈을 __main__ 네임스페이스에서 실행하고 패키지 이름은 평소처럼 __init__ 파일을 참조함으로써 둘을 분리해 두는 것과 마찬가지입니다.
배경
저는 이름 있는 모듈, 즉 메인 프로그램 모듈을 몽키 패치하려는 모듈을 통해 메인 프로그램을 디버깅하던 중 이 문제를 겪었습니다(I tripped over this issue). 당연히 몽키 패치는 효과가 없었는데, 메인 모듈을 이름으로 임포트했기 때문에 실행 중인 모듈 인스턴스가 아니라 두 번째 모듈 인스턴스를 패치했기 때문입니다.
그러나 이 문제는 -m 명령줄 옵션만큼이나 오래전부터 존재해 왔으며, 자주는 아니더라도 다른 사람들도 꾸준히 겪고 있습니다.
issue 19702 외에도, __main__을 둘러싼 불일치는 PEP 451에서 암시되어 있으며, (PEP 451보다 앞선) 유사한 제안이 PEP 395의 메인 모듈의 이중 임포트 수정 항목에 설명되어 있습니다.
참고 문헌
Copyright
This document has been placed in the public domain.