PEP 432 – CPython 시작 절차 재구성
- Author:
- Alyssa Coghlan <ncoghlan at gmail.com>, Victor Stinner <vstinner at python.org>, Eric Snow <ericsnowcurrently at gmail.com>
- Discussions-To:
- Capi-SIG list
- Status:
- Withdrawn
- Type:
- Standards Track
- Requires:
- 587
- Created:
- 28-Dec-2012
- Post-History:
- 28-Dec-2012, 02-Jan-2013, 30-Mar-2019, 28-Jun-2020
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
PEP 철회
2012년 말부터 2020년 중반까지 이 PEP는 CPython 시작 절차를 더 쉽게 유지 관리하고, CPython 런타임을 더 큰 애플리케이션의 일부로 더 쉽게 임베딩할 수 있도록 하기 위한 전반적인 배경과 구체적인 제안을 제공했습니다.
그 기간의 대부분 동안 이러한 변경 사항은 별도의 기능 브랜치에서 유지 관리되거나, 주 CPython 저장소에서 밑줄로 시작하는 비공개 API로 유지 관리되었습니다.
2019년에 PEP 587은 이러한 API 변경 사항 중 일부를 Python 3.8 이상의 공개 CPython API로 옮겼습니다(구체적으로, 해당 PEP는 인터프리터 런타임이 명시적인 다단계 구조체 기반 설정 인터페이스를 제공하도록 갱신했습니다).
2020년 6월, 운영 위원회(Steering Council)의 문의에 대한 응답으로 PEP 작성자들은 원래 PEP를 철회하는 것이 타당하다고 결정했습니다. PEP 432가 처음 작성된 이후로 충분히 많은 것이 바뀌었기 때문에, 시작 절차와 임베딩 API에 대한 추가 변경은 새로운 PEP(들)로 작성하는 것이 가장 좋다고 판단했습니다. 그 새로운 PEP는 PEP 432의 아이디어 중 충분히 검증되지 않았다고 여겨져 PEP 587에 포함되지 못한 미구현 아이디어뿐만 아니라, 공개된 PEP 587 API에 대한 피드백, 그리고 CPython 구현을 임베딩과 서브인터프리터에 더 친화적으로 조정하는 과정에서 얻은 그 밖의 교훈까지 고려해야 합니다.
특히 다음과 같은 변경을 제안하는 PEP와 이를 가능하게 하는 데 필요한 추가 인프라 변경은 여전히 검토할 가치가 있을 것입니다:
- 모든 사용자 수준 설정을 무시하고 기본적으로 격리 모드로 실행되어, 기본 인터프리터보다 시스템 수준 Python 애플리케이션 실행에 더 적합한 대체 Python 실행 파일을 함께 제공하는 것
- 순수 Python 스크립트로부터 단일 파일 실행 파일을 만들 수 있도록 zipapp 모듈을 개선하는 것(다단계 확장 모듈 초기화가 도입되었으므로 잠재적으로는 Python 확장 모듈까지도 포함)
- 테스트 스위트 커버리지와 해당 코드의 전반적인 유지 관리성을 향상하기 위해 복잡한 sys.path 초기화 로직을 C에서 Python으로 옮기는 것
초록
이 PEP는 CPython의 시작 절차를 재구성하는 메커니즘을 제안합니다. 이를 통해 참조 인터프리터 실행 파일의 초기화 동작을 더 쉽게 수정할 수 있고, 대체 실행 파일을 만들거나 CPython을 더 큰 애플리케이션 내부에 Python 실행 엔진으로 임베딩할 때 CPython의 시작 동작을 더 쉽게 제어할 수 있습니다.
이 제안의 구현이 완료되면 인터프리터 시작은 명확히 구분되고 독립적으로 설정할 수 있는 세 단계로 구성됩니다:
- Python 코어 런타임 사전 초기화
- 메모리 관리 설정
- 시스템 인터페이스에 사용할 인코딩 결정(이후 설정 단계를 위해 전달되는 설정 포함)
- Python 코어 런타임 초기화
- C API를 사용할 준비가 되었는지 보장
- 내장 모듈과 동결(frozen) 모듈에 접근할 수 있는지 보장
- 주 인터프리터 설정
- 외부 모듈에 접근할 수 있는지 보장
- (참고: 이 단계의 이름은 변경될 가능성이 매우 높습니다)
주 모듈 실행과 서브인터프리터 초기화에 영향을 주는 변경도 함께 제안합니다.
참고: TBC = To Be Confirmed(확인 예정), TBD = To Be Determined(결정 예정)입니다. 이들 대부분에 대한 적절한 해결 방안은 참조 구현이 개발됨에 따라 더 명확해질 것입니다.
제안
이 PEP는 CPython 런타임의 초기화를 명확히 구분되는 세 단계로 나눌 것을 제안합니다:
- 코어 런타임 사전 초기화
- 코어 런타임 초기화
- 주 인터프리터 설정
(이전 버전에서는 두 단계만 제안했지만, 이 PEP를 CPython 내부 리팩터링으로 구현하려 한 경험을 통해 관심사를 명확히 분리하려면 최소 3단계가 필요하다는 점이 드러났습니다)
제안된 설계는 다음 사항에도 중요한 영향을 미칩니다:
- 메인 모듈 실행
- 서브인터프리터 초기화
새 설계에서 인터프리터는 초기화 시퀀스 동안 다음과 같이 명확하게 정의된 단계를 거칩니다:
- 초기화되지 않음(Uninitialized) - 아직 사전 초기화 단계조차 시작하지 않은 상태
- 사전 초기화(Pre-Initialization) - 사용 가능한 인터프리터가 없는 상태
- 런타임 초기화됨(Runtime Initialized) - 메인 인터프리터를 부분적으로 사용할 수 있으나, 서브인터프리터 생성은 아직 불가능한 상태
- 초기화됨(Initialized) - 메인 인터프리터를 완전히 사용할 수 있으며, 서브인터프리터 생성도 가능한 상태
PEP 587은 사전 초기화 단계를 마지막 두 단계와 분리하는 내용을 다루는 더 상세한 제안이지만, 임베딩 애플리케이션이 “런타임 초기화됨” 상태에서 임의의 코드를 실행하는 것은 허용하지 않습니다(대신 코어 런타임을 초기화하면 항상 메인 인터프리터도 완전히 초기화됩니다. Python 3.8의 네이티브 CPython CLI가 여전히 그렇게 동작하기 때문입니다).
설계 변경의 방향을 잡는 데 도움이 될 구체적인 사용 사례로 삼고, 시스템 유틸리티에 적합한 기본값이 사용자 스크립트 실행에 적합한 기본값과 다르다는 알려진 문제를 해결하기 위해, 이 PEP는 기본적으로 (CPython -I 스위치로 선택되는) “격리 모드”로 동작하는 별도의 시스템 Python(system-python) 실행 파일을 만들어 배포할 것을 제안합니다. 또한 일반적인 CPython 시작 시퀀스를 거치는 대신 덧붙여진 zip 아카이브를 실행하기만 하는(단일 파일 순수 Python 실행 파일을 가능하게 하는) 예제 스텁 바이너리를 만들 것도 제안합니다.
구현 복잡도를 통제하기 위해, 이 PEP는 런타임에 인터프리터 상태에 접근하는 방식을 전면적으로 변경하는 것은 제안하지않습니다. 시작 시퀀스를 유지 보수하기 쉽게 만들기 위해 기존 초기화 단계가 일어나는 순서를 바꾸는 것만으로도 이미 상당한 변경이며, 그 밖의 변경까지 동시에 시도하면 변경이 훨씬 더 침습적이 되고 리뷰하기도 훨씬 어려워집니다. 다만 그러한 제안은 후속 PEP나 패치에 적합한 주제가 될 수 있습니다. 이 PEP와 관련 하위 제안들의 핵심 이점 중 하나는 내부 저장 모델과 구성 인터페이스 사이의 결합도를 낮추는 것이므로, 이 PEP가 구현되고 나면 그러한 변경도 더 쉬워질 것입니다.
배경
시간이 지나면서 CPython의 초기화 순서는 점점 더 복잡해졌습니다. 더 많은 옵션을 제공하게 되었고, 더 복잡한 작업도 수행하게 되었습니다(Python 3에서 OS 인터페이스를 위한 유니코드 설정 구성 [10], 임포트 시스템의 순수 Python 구현 부트스트래핑, 높은 권한으로 실행되는 시스템 애플리케이션에 더 적합한 격리 모드 구현 [6] 등).
이러한 복잡성의 상당 부분은 공식적으로 Py_Main과 Py_Initialize API를 통해서만 접근할 수 있어, 임베딩 애플리케이션이 커스터마이즈할 여지가 거의 없습니다. 이렇게 점점 늘어나는 복잡성은 유지 관리자도 힘들게 합니다. 설정의 상당 부분이 Py_Initialize 호출 이전에 이루어져야 하므로 Python C API의 상당 부분을 안전하게 사용할 수 없기 때문입니다.
sys.path 초기화를 더 잘 제어하는 것(예: 명령줄에서 크로스 플랫폼 방식으로 추가 디렉터리를 쉽게 추가하는 것 [7], sys.path[0] 구성을 제어하는 것 [8])이나, Python 하위 프로세스를 실행할 때 커버리지 추적 같은 유틸리티를 더 쉽게 구성하는 것 [9] 등, 더욱 정교한 시작 동작을 위한 여러 제안이 논의되고 있습니다.
이 PEP는 이미 복잡한 시스템에 그러한 동작을 끝없이 덧붙여 나가는 대신, 더 구조화된 시작 시퀀스를 도입하여 현 상태를 단순화하기 시작할 것을 제안합니다. 이를 통해 이러한 추가 기능 요청을 더 쉽게 구현할 수 있도록 하는 것이 목표입니다.
원래는 전체 제안을 이 PEP 하나에서 관리했지만 이는 비현실적인 것으로 드러났습니다. 그래서 제안된 설계의 일부가 안정화됨에 따라 이를 별도의 PEP로 분리하였으며, 덕분에 전체 설계의 세부 사항이 여전히 발전하는 중에도 진척을 이룰 수 있게 되었습니다.
핵심 고려 사항
시작 시퀀스를 변경할 때는 몇 가지 핵심 고려 사항을 반드시 감안해야 합니다.
유지 보수성
Python 3.6 시점의 CPython 시작 시퀀스는 이해하기 어려웠고, 수정하기는 더욱 어려웠습니다. 초기화 코드의 상당 부분이 실행되는 동안 인터프리터가 어떤 상태에 있는지 명확하지 않았으며, 이로 인해 -X 또는 -W 옵션을 사용하면 Py_Initialize 호출 전에 리스트, 딕셔너리, 유니코드 값이 생성되는 등의 동작이 발생했습니다 [1].
명시적인 다단계 시작 시퀀스로 전환하면, 개발자는 다음 사항만 이해하면 됩니다:
- 사전 구성 전에 사용할 수 있는 API와 기능(사전 구성 API 자체를 제외하면 사실상 없음)
- 코어 런타임 구성 전에 사용할 수 있으며, 사전 구성이 명시적으로 실행되지 않은 경우 Python 3.6의 동작과 일치하는 기본 설정으로 사전 구성을 암묵적으로 실행하는 API와 기능
- 메인 인터프리터가 완전히 구성된 후에만 사용할 수 있는 API와 기능(전체 C API 중 비교적 작은 부분 집합이 되기를 기대함)
이 중 처음 두 가지 측면은 PEP 587에서 다루며, 마지막 구분의 세부 사항은 아직 검토 중입니다.
새 설계를 C 구조체와 Python 데이터 타입의 조합에 기반하도록 하면, 향후 새로운 구성 옵션을 추가하기 위해 시스템을 수정하는 것도 더 쉬워질 것입니다.
테스트 용이성
CPython 시작 시퀀스의 복잡성이 가진 문제 중 하나는 서로 다른 구성 설정 간에 가능한 상호 작용이 조합적으로 폭발한다는 점입니다.
이 문제는 새 초기화 시스템의 설계뿐 아니라, 그 시스템에 도달하기 위해 제안된 접근 방식에도 영향을 미칩니다.
성능
CPython은 런타임의 대부분을 인터프리터 초기화 시간이 차지하는 짧은 스크립트를 실행하는 데 많이 사용됩니다. 시작 순서를 변경할 때는 시작 오버헤드에 미치는 영향을 최소화해야 합니다.
importlib 마이그레이션 경험에 따르면 시작 시간의 대부분은 IO 연산이 차지합니다. 그러나 변경 사항의 영향을 모니터링하기 위해, 인터프리터를 시작했다가 해체하는 데 걸리는 시간을 확인하는 간단한 벤치마크를 사용할 수 있습니다:
python3 -m timeit -s "from subprocess import call" "call(['./python', '-Sc', 'pass'])"
제 시스템에서 (Fedora 프로젝트가 빌드한) Python 3.7로 측정한 현재 수치는 다음과 같습니다:
$ python3 -m timeit -s "from subprocess import call" "call(['python3', '-Sc', 'pass'])"
50 loops, best of 5: 6.48 msec per loop
(TODO: 이 마이크로벤치마크를 표준 라이브러리 timeit 대신 perf로 실행하기)
이 PEP는 개별 단계를 크게 변경하지 않고 주로 기존 초기화 순서를 재배치하는 것을 목표로 하므로, 시작 시간에 큰 영향을 미치지 않을 것으로 예상됩니다.
그러나 이 간단한 확인 결과 초기화 순서에 대해 제안된 변경 사항이 성능 문제를 일으킬 수 있다고 판단되면, 조사를 돕기 위해 더 정교한 마이크로벤치마크를 개발할 것입니다.
필수 구성 설정
CPython 인터프리터 구성 설정의 상세 목록과 이를 설정하는 데 사용할 수 있는 다양한 수단은 PEP 587을 참조하십시오.
구현 전략
이 PEP의 이전 버전을 Python 3.4용으로 구현하려는 초기 시도가 있었으며 [2], 그때 겪은 중대한 문제 중 하나는 리팩터링 과정을 시작하기 위해 초기 구조 변경을 적용한 이후 발생한 병합 충돌이었습니다. Python 2.5에서 AST 기반 컴파일러로 전환하거나 Python 3.3에서 임포트 시스템을 importlib 구현으로 전환한 것과 같은 이전의 다른 주요 변경과 달리, 최초 시도를 괴롭혔던 종류의 병합 충돌에 취약하지 않도록 초안 구현을 구성할 명확한 방법이 없습니다.
이에 따라 구현 전략을 수정하여, 먼저 이 리팩터링을 CPython 3.7의 비공개 API로 구현한 다음, 새 함수와 구조체를 CPython 3.8에서 공개 API 요소로 노출하는 것이 타당한지 검토하기로 했습니다.
초기 병합 이후, Victor Stinner는 PEP 540 UTF-8 모드 변경을 성공적으로 구현하기 위해 실제로 설정을 새 구조로 마이그레이션하는 작업을 진행했습니다(이 변경에는 이전에 로케일 인코딩으로 디코딩되었던 모든 설정을 추적하고, 이를 대신 UTF-8로 다시 디코딩하는 기능이 필요했습니다). Eric Snow도 서브인터프리터 기능을 더 견고하게 만드는 작업의 일환으로 여러 내부 서브시스템을 마이그레이션했습니다.
그 작업을 통해 이 PEP에서 원래 제안한 상세 설계에 여러 실제적인 문제가 있음이 드러났고, 이에 Victor는 (이 PEP의 이전 버전에서 영감을 받아) 개선된 비공개 API를 설계하고 구현했으며, PEP 587은 이를 Python 3.8에서 공개 API로 승격할 것을 제안합니다.
설계 세부 사항
Note
여기에 나온 API 세부 사항은 아직도 크게 변동하고 있습니다. 비공개 API의 현재 상태를 보여 주는 헤더 파일은 주로 다음과 같습니다:
- https://github.com/python/cpython/blob/master/Include/cpython/coreconfig.h
- https://github.com/python/cpython/blob/master/Include/cpython/pystate.h
- https://github.com/python/cpython/blob/master/Include/cpython/pylifecycle.h
PEP 587은 공개할 수 있을 만큼 안정적일 가능성이 있다고 여겨지는 API 측면을 다룹니다. 제안된 API가 해당 PEP에서 다루어지는 경우, 아래 본문에 “(PEP 587 참조)”를 덧붙였습니다.
이 제안의 핵심 주제는 시작 과정에서 훨씬 더 이른 시점에 코어 언어 런타임을 초기화하고, 메인 인터프리터를 위한 부분적으로 초기화된 인터프리터 상태를 생성하는 것입니다. 이렇게 하면 나머지 초기화 과정에서 CPython API의 대부분을 사용할 수 있게 됩니다. 따라서 현재 CPython C API가 제공하는 더 풍부한 자료 구조를 사용하지 못하고 기본적인 C 기능에 의존해야 하는 여러 작업을 단순화할 수 있습니다.
PEP 587은 그 작업의 일부를 다루는데, 이는 기존의 “Py_Initialize 이전에 호출될 수 있음” 인터페이스조차 필요로 하는 구성 요소(메모리 할당자와 운영 체제 인터페이스 인코딩 세부 사항 등)를 별도의 사전 구성 단계로 분리하는 것입니다.
이하에서 “임베딩 애플리케이션”이라는 용어는 표준 CPython 명령줄 애플리케이션도 포함합니다.
인터프리터 초기화 단계
다음과 같이 서로 구별되는 인터프리터 초기화 단계를 제안합니다:
- 초기화되지 않음(Uninitialized):
- 실제로는 단계라기보다 단계가 없는 상태입니다
Py_IsInitializing()은0을 반환합니다Py_IsRuntimeInitialized()는0을 반환합니다Py_IsInitialized()는0을 반환합니다- 임베딩 애플리케이션은 어떤 메모리 할당자를 사용할지, 그리고 운영 체제 인터페이스에 접근할 때 어떤 인코딩을 사용할지 결정합니다(또는 이러한 결정을 Python 런타임에 위임하기로 선택합니다)
- 애플리케이션은
Py_PreInitializeAPI 중 하나를 호출하여 초기화 과정을 시작합니다(PEP 587 참조)
- 런타임 사전 초기화(Runtime Pre-Initialization):
- 사용 가능한 인터프리터가 없습니다
Py_IsInitializing()은1을 반환합니다Py_IsRuntimeInitialized()는0을 반환합니다Py_IsInitialized()는0을 반환합니다- 임베딩 애플리케이션은 핵심 CPython 런타임을 초기화하고 메인 인터프리터를 생성하는 데 필요한 설정을 결정하며,
Py_InitializeRuntime을 호출하여 다음 단계로 넘어갑니다 - 참고: PEP 587부터는 임베딩 애플리케이션이 그 대신
Py_Main(),Py_UnixMain또는Py_InitializeAPI 중 하나를 호출하므로 초기화 완료(Initialized) 상태로 곧바로 넘어갑니다.
- 메인 인터프리터 초기화(Main Interpreter Initialization):
- 내장 데이터 타입과 그 밖의 핵심 런타임 서비스를 사용할 수 있습니다
- 메인 인터프리터를 사용할 수 있지만, 부분적으로만 구성된 상태입니다
Py_IsInitializing()은1을 반환합니다Py_IsRuntimeInitialized()는1을 반환합니다Py_IsInitialized()는0을 반환합니다- 임베딩 애플리케이션은
Py_InitializeMainInterpreter를 호출하여 초기화 과정을 완료하는 데 필요한 설정을 결정하고 적용합니다 - 참고: PEP 587부터는 이 상태에 어떤 공개 API로도 도달할 수 없으며,
Py_Initialize함수 중 하나가 실행되는 동안에만 암묵적인 내부 상태로 존재합니다
- 초기화 완료(Initialized):
- 메인 인터프리터를 사용할 수 있고 완전히 작동하지만,
__main__관련 메타데이터는 불완전합니다 Py_IsInitializing()은0을 반환합니다Py_IsRuntimeInitialized()는1을 반환합니다Py_IsInitialized()는1을 반환합니다
- 메인 인터프리터를 사용할 수 있고 완전히 작동하지만,
단계의 호출
나열된 모든 단계는 표준 CPython 인터프리터와 제안된 System Python 인터프리터에서 사용됩니다.
임베딩 애플리케이션은 기존의 Py_Initialize 또는 Py_Main() API를 사용하여 초기화를 거의 전적으로 CPython의 제어에 계속 맡길 수 있습니다. 하위 호환성은 유지됩니다.
또는 임베딩 애플리케이션이 CPython의 초기 상태를 더 많이 제어하고자 한다면, 새롭고 더 세분화된 API를 사용할 수 있습니다. 이 API를 통해 임베딩 애플리케이션은 초기화 과정을 더 세밀하게 제어할 수 있습니다.
PEP 587은 해당 API의 초기 버전을 다룹니다. 이 버전은 사전 초기화 단계를 분리하지만, 코어 런타임 초기화와 주 인터프리터 초기화는 분리하려고 하지 않습니다.
초기화되지 않은 상태
초기화되지 않은 상태에서 임베딩 애플리케이션은 필요한 설정을 결정합니다. 이 설정이 있어야 임베드된 Python 런타임에 구성 설정을 올바르게 전달할 수 있습니다.
여기에는 Python이 사용할 메모리 할당자를 지정하는 일과, 제공된 설정을 처리할 때 사용할 텍스트 인코딩을 지정하는 일이 포함됩니다.
PEP 587은 이 상태를 벗어나는 데 필요한 설정을 PyPreConfig 구조체에 정의합니다.
새로운 조회 API를 사용하면 인터프리터가 초기화 과정을 아직 시작하지도 않았는지 코드에서 판단할 수 있습니다:
int Py_IsInitializing();
그러면 완전히 초기화되지 않은 환경에 대한 조회는 !(Py_Initialized() || Py_Initializing()) 형태가 됩니다.
런타임 사전 초기화 단계
Note
PEP 587에서는 이 단계의 설정이 아직 분리되어 있지 않으며,
대신 통합된 PyConfig 구조체를 통해서만 사용할 수 있습니다
사전 초기화 단계에서 임베딩 애플리케이션은 CPython 런타임을 초기화하기 전에 반드시 있어야 하는 설정을 결정합니다. 현재 이 범주에 속하는 주요 구성 설정은 무작위 해시 알고리즘과 관련된 설정입니다. 해시 알고리즘은 프로세스가 살아 있는 동안 일관되게 유지되어야 하므로, 코어 인터프리터를 생성하기 전에 설정되어 있어야 합니다.
필수 설정은 두 가지입니다. 하나는 무작위 해시에 특정 시드 값을 사용할지를 나타내는 플래그이고, 다른 하나는 특정 시드 값을 사용하는 경우의 해당 값입니다(시드 값이 0이면 무작위 해싱이 비활성화됩니다). 또한 해시 무작위화를 구성할 때 PYTHONHASHSEED를 사용할 수 있으므로, 환경 변수를 고려할지도 초기에 결정해야 합니다. 마지막으로, CPython 빌드 과정을 지원하기 위해 임포트 시스템을 완전히 비활성화하는 옵션을 제공합니다.
시작 순서의 이 단계에 대해 제안하는 API는 다음과 같습니다:
PyInitError Py_InitializeRuntime(
const PyRuntimeConfig *config
);
PyInitError Py_InitializeRuntimeFromArgs(
const PyRuntimeConfig *config, int argc, char **argv
);
PyInitError Py_InitializeRuntimeFromWideArgs(
const PyRuntimeConfig *config, int argc, wchar_t **argv
);
Py_IsInitializing()이 거짓이면 Py_InitializeRuntime 함수들은 대응하는 Py_PreInitialize 함수를 암묵적으로 호출합니다. use_environment 설정은 그대로 전달되며, 다른 설정은 PEP 587에 설명된 대로 기본값에 따라 처리됩니다.
PyInitError 반환 타입은 PEP 587에 정의되어 있습니다. 이 타입을 사용하면 임베딩 애플리케이션이 Python 런타임 초기화 실패를 우아하게 처리할 수 있으므로, 전체 프로세스가 Py_FatalError에 의해 갑자기 종료되지 않습니다.
새로운 PyRuntimeConfig 구조체는 코어 런타임의 예비 구성과 주 인터프리터 생성에 필요한 설정을 담습니다:
/* Note: if changing anything in PyRuntimeConfig, also update
* PyRuntimeConfig_INIT */
typedef struct {
bool use_environment; /* as in PyPreConfig, PyConfig from PEP 587 */
int use_hash_seed; /* PYTHONHASHSEED, as in PyConfig from PEP 587 */
unsigned long hash_seed; /* PYTHONHASHSEED, as in PyConfig from PEP 587 */
bool _install_importlib; /* Needed by freeze_importlib */
} PyRuntimeConfig;
/* Rely on the "designated initializer" feature of C99 */
#define PyRuntimeConfig_INIT {.use_hash_seed=-1}
코어 구성 설정 포인터는 NULL일 수 있으며, 이 경우 PyRuntimeConfig_INIT에 지정된 값이 기본값으로 사용됩니다.
PyRuntimeConfig_INIT 매크로는 구조체 인스턴스를 적절한 기본값으로 쉽게 초기화할 수 있도록 설계되었습니다:
PyRuntimeConfig runtime_config = PyRuntimeConfig_INIT;
use_environment는 Python 관련 환경 변수 전체의 처리를 제어합니다. 플래그가 참이면 PYTHONHASHSEED가 정상적으로 처리됩니다. 그렇지 않으면 Python 전용 환경 변수는 모두 정의되지 않은 것으로 간주됩니다(일부 OS 전용 환경 변수는 예외가 될 수 있습니다. 예를 들어 Mac OS X에서 App 번들과 주 Python 바이너리가 통신하는 데 사용하는 환경 변수가 이에 해당합니다).
use_hash_seed는 무작위 해시 알고리즘의 구성을 제어합니다. 이 값이 0이면 무작위 시드를 사용하는 무작위 해시가 사용됩니다. 양수이면 hash_seed의 값이 난수 제너레이터의 시드로 사용됩니다. 이 경우 hash_seed가 0이면 무작위 해싱이 완전히 비활성화됩니다.
use_hash_seed가 음수이면(그리고 use_environment가 참이면) CPython은 PYTHONHASHSEED 환경 변수를 검사합니다. 환경 변수가 설정되지 않았거나, 빈 문자열 또는 "random" 값으로 설정되었으면 무작위 시드를 사용하는 무작위화된 해시가 사용됩니다. 환경 변수가 문자열 "0"으로 설정되었으면 해시 무작위화가 비활성화됩니다. 그 밖의 경우, 해시 시드는 [0; 4294967295] 범위에 있는 정수의 문자열 표현이어야 합니다.
임베딩 애플리케이션이 다른 데이터 소스와 함께 PYTHONHASHSEED 처리를 더 쉽게 사용할 수 있도록, 다음 헬퍼 함수가 C API에 추가됩니다:
int Py_ReadHashSeed(char *seed_text,
int *use_hash_seed,
unsigned long *hash_seed);
이 함수는 seed_text로 시드 문자열을 받아 적절한 플래그 값과 시드 값으로 변환합니다. seed_text가 NULL이거나 빈 문자열 또는 "random" 값이면 use_hash_seed와 hash_seed가 모두 0으로 설정됩니다. 그렇지 않으면 use_hash_seed는 1로 설정되고, 시드 텍스트는 정수로 해석되어 hash_seed로 보고됩니다. 성공하면 함수는 0을 반환합니다. 0이 아닌 반환값은 오류를 나타냅니다(대부분 정수로 변환하는 과정에서 발생한 오류입니다).
_install_importlib 설정은 CPython 빌드 과정의 일부로, 임포트 기능이 전혀 없는 인터프리터를 만드는 데 사용됩니다. 이 설정은 CPython 개발 팀 전용으로 간주되는데(그래서 이름이 밑줄로 시작합니다), 현재 지원되는 유일한 사용 사례가 importlib._bootstrap의 기존 프리즈된 바이트코드를 무효화하는 컴파일러 변경을 빌드 과정을 깨뜨리지 않고 허용하는 것이기 때문입니다.
목표는 이 초기 수준의 구성을 가능한 한 작게 유지하여 여러 임베딩 애플리케이션에서 부트스트래핑 환경을 일관되게 유지하는 것입니다. 이 설정 없이도 유효한 인터프리터 상태를 만들 수 있다면, 이 설정은 코어 런타임 구성이 아니라 포괄적인 PyConfig 구조체에만 있어야 합니다.
새 조회 API를 사용하면 코드에서 인터프리터가 코어 런타임 초기화 이후부터 주 인터프리터 상태 생성 및 주 인터프리터 초기화 과정 대부분의 완료 전까지의 부트스트래핑 상태에 있는지 판단할 수 있습니다:
int Py_IsRuntimeInitialized();
Py_IsRuntimeInitialized()가 이미 참일 때 Py_InitializeRuntime()을 다시 호출하려고 하면 사용자 구성 오류로 보고됩니다. (추후 확정(TBC): 기존 공개 초기화 API는 여러 번 호출되어도 오류 없이 동작하며, 한 번만 쓸 수 있는 설정에 대한 변경은 그냥 무시하기 때문입니다. 새 API를 기존 API보다 엄격하게 만들려 하기보다는 그 동작을 유지하는 편이 타당할 수도 있습니다)
이제 아직 완전히 초기화되지 않은 인터프리터에서도 프리즈된 바이트코드가 정당하게 실행될 수 있으므로, sys.flags에 새 initialized 플래그가 추가됩니다.
코어 런타임이 초기화되면 주 인터프리터와 CPython C API 대부분이 완전히 동작해야 하지만, 다음과 같은 예외가 있습니다:
- 컴파일은 허용되지 않습니다(파서와 컴파일러가 아직 제대로 구성되지 않았기 때문입니다)
- 서브인터프리터 생성은 허용되지 않습니다
- 추가 스레드 상태 생성은 허용되지 않습니다
sys모듈의 다음 속성은 모두 존재하지 않거나None입니다: *sys.path*sys.argv*sys.executable*sys.base_exec_prefix*sys.base_prefix*sys.exec_prefix*sys.prefix*sys.warnoptions*sys.dont_write_bytecode*sys.stdin*sys.stdout- 파일 시스템 인코딩은 아직 정의되지 않았습니다
- IO 인코딩은 아직 정의되지 않았습니다
- CPython 시그널 핸들러는 아직 설치되지 않습니다
- 내장 모듈과 프리즈된 모듈만 임포트할 수 있습니다(위의 제약 때문입니다)
sys.stderr는 버퍼링되지 않는 바이너리 모드를 사용하는 임시 IO 객체로 설정됩니다sys.flags속성은 존재하지만, 개별 플래그는 아직 최종 값을 갖지 않을 수 있습니다.sys.flags.initialized속성은0으로 설정됩니다warnings모듈은 아직 초기화되지 않았습니다__main__모듈은 아직 존재하지 않습니다
<TBD: 그 밖에 주목할 만한 누락 기능 파악>
이 단계에서 사용할 수 있게 되는 주요 요소는 핵심 Python 데이터 타입이며, 특히 딕셔너리, 리스트, 문자열이 여기에 해당합니다. 따라서 (현재 상태와 달리) 나머지 모든 구성 단계에서 이들을 안전하게 사용할 수 있습니다.
또한 현재 스레드는 유효한 Python 스레드 상태를 갖게 됩니다. 따라서 이후의 설정 데이터는 C 프로세스 전역 변수가 아니라 메인 인터프리터 객체에 저장할 수 있습니다.
Py_InitializeRuntime() 호출에는 반드시 이에 대응하는 Py_Finalize() 호출이 있어야 합니다. 그 사이에 Py_InitializeMainInterpreter() 호출을 건너뛰어도 됩니다(예: 메인 인터프리터 구성 설정을 만들려는 시도가 실패한 경우).
나머지 구성 설정 결정하기
초기화 순서의 다음 단계는 과정을 완료하는 데 필요한 나머지 설정을 결정하는 것입니다. 이 시점에는 인터프리터 상태를 변경하지 않습니다. 이 단계의 핵심 API는 다음과 같습니다:
int Py_BuildPythonConfig(
PyConfigAsObjects *py_config, const PyConfig *c_config
);
int Py_BuildPythonConfigFromArgs(
PyConfigAsObjects *py_config, const PyConfig *c_config, int argc, char **argv
);
int Py_BuildPythonConfigFromWideArgs(
PyConfigAsObjects *py_config, const PyConfig *c_config, int argc, wchar_t **argv
);
py_config 인자는 PyConfigAsObjects 구조체를 가리키는 포인터여야 합니다(C 스택에 저장된 임시 구조체여도 됩니다). 이미 구성된 값(즉, NULL이 아닌 포인터)에 대해서는 CPython이 제공된 값의 타당성을 검사하지만, 그 밖에는 올바른 값으로 받아들입니다.
파이썬 딕셔너리 대신 구조체를 사용하는 이유는 구조체가 C에서 다루기 더 쉽고, 지원되는 필드 목록이 CPython 버전마다 고정되어 있으며, 파이썬 코드에는 읽기 전용 뷰만 노출하면 되기 때문입니다(sys.implementation을 노출하려고 이미 마련해 둔 기반 구조 덕분에 이는 비교적 간단합니다).
Py_InitializeRuntime과 달리, 이 호출은 설정 데이터에서 문제가 발견되면 Python 초기화 전용 C 구조체를 반환하지 않습니다. 대신 Python 예외를 발생시키고 오류를 반환합니다.
지원되는 구성 설정 중 아직 설정되지 않은 항목은 제공된 구성 구조체에 적절한 값으로 채워집니다. Py_BuildPythonConfig를 호출하기 전에 값을 설정하면 기본 구성을 완전히 재정의할 수 있습니다. 그러면 제공된 값은 그 값에서 파생되는 다른 설정을 계산할 때도 사용됩니다.
또는 Py_BuildPythonConfig 호출 후에 설정을 재정의할 수도 있습니다(임베딩 애플리케이션이 설정을 완전히 대체하지 않고 조정하려는 경우, 예를 들어 sys.path[0]을 제거하려는 경우에 유용할 수 있습니다).
c_config 인자는 PEP 587에 정의된 PyConfig 구조체를 가리키는 선택적 포인터입니다. 이 인자가 제공되면 환경이나 프로세스 전역 상태에서 설정을 직접 읽는 것보다 우선하여 사용됩니다.
구성을 읽는 것만으로는 인터프리터 상태에 아무런 영향을 주지 않습니다. 전달된 구성 구조체만 수정할 뿐입니다. 설정은 Py_InitializeMainInterpreter 호출(아래 참조) 전까지는 실행 중인 인터프리터에 적용되지 않습니다.
지원되는 구성 설정
인터프리터 구성은 두 부분으로 나뉩니다. 하나는 메인 인터프리터에만 관련되거나 메인 인터프리터와 모든 서브인터프리터에서 동일해야 하는 설정이고, 다른 하나는 서브인터프리터마다 다를 수 있는 설정입니다.
참고: 초기 구현에서는 해당 인터프리터가 메인 인터프리터인지 여부를 나타내는 플래그만 인터프리터별로 구성합니다. 다른 필드는 구현을 진행하면서 인터프리터별로 만드는 것이 실현 가능한지 검토할 예정입니다.
PyConfigAsObjects 구조체는 PEP 587의 PyConfig 구조체를 그대로 따르지만, 값을 저장할 때 C 수준의 데이터 타입 대신 완전한 Python 객체를 사용합니다. 또한 raw_argv와 argv 리스트 필드를 추가하므로, 이후의 초기화 단계에서 이들을 별도로 받을 필요가 없습니다.
필드는 항상 Python 데이터 타입을 가리키는 포인터이며, 설정되지 않은 값은 NULL로 표시됩니다:
typedef struct {
/* Argument processing */
PyListObject *raw_argv;
PyListObject *argv;
PyListObject *warnoptions; /* -W switch, PYTHONWARNINGS */
PyDictObject *xoptions; /* -X switch */
/* Filesystem locations */
PyUnicodeObject *program_name;
PyUnicodeObject *executable;
PyUnicodeObject *prefix; /* PYTHONHOME */
PyUnicodeObject *exec_prefix; /* PYTHONHOME */
PyUnicodeObject *base_prefix; /* pyvenv.cfg */
PyUnicodeObject *base_exec_prefix; /* pyvenv.cfg */
/* Site module */
PyBoolObject *enable_site_config; /* -S switch (inverted) */
PyBoolObject *no_user_site; /* -s switch, PYTHONNOUSERSITE */
/* Import configuration */
PyBoolObject *dont_write_bytecode; /* -B switch, PYTHONDONTWRITEBYTECODE */
PyBoolObject *ignore_module_case; /* PYTHONCASEOK */
PyListObject *import_path; /* PYTHONPATH (etc) */
/* Standard streams */
PyBoolObject *use_unbuffered_io; /* -u switch, PYTHONUNBUFFEREDIO */
PyUnicodeObject *stdin_encoding; /* PYTHONIOENCODING */
PyUnicodeObject *stdin_errors; /* PYTHONIOENCODING */
PyUnicodeObject *stdout_encoding; /* PYTHONIOENCODING */
PyUnicodeObject *stdout_errors; /* PYTHONIOENCODING */
PyUnicodeObject *stderr_encoding; /* PYTHONIOENCODING */
PyUnicodeObject *stderr_errors; /* PYTHONIOENCODING */
/* Filesystem access */
PyUnicodeObject *fs_encoding;
/* Debugging output */
PyBoolObject *debug_parser; /* -d switch, PYTHONDEBUG */
PyLongObject *verbosity; /* -v switch */
/* Code generation */
PyLongObject *bytes_warnings; /* -b switch */
PyLongObject *optimize; /* -O switch */
/* Signal handling */
PyBoolObject *install_signal_handlers;
/* Implicit execution */
PyUnicodeObject *startup_file; /* PYTHONSTARTUP */
/* Main module
*
* If prepare_main is set, at most one of the main_* settings should
* be set before calling PyRun_PrepareMain (Py_ReadMainInterpreterConfig
* will set one of them based on the command line arguments if
* prepare_main is non-zero when that API is called).
PyBoolObject *prepare_main;
PyUnicodeObject *main_source; /* -c switch */
PyUnicodeObject *main_path; /* filesystem path */
PyUnicodeObject *main_module; /* -m switch */
PyCodeObject *main_code; /* Run directly from a code object */
PyObject *main_stream; /* Run from stream */
PyBoolObject *run_implicit_code; /* Run implicit code during prep */
/* Interactive main
*
* Note: Settings related to interactive mode are very much in flux.
*/
PyObject *prompt_stream; /* Output interactive prompt */
PyBoolObject *show_banner; /* -q switch (inverted) */
PyBoolObject *inspect_main; /* -i switch, PYTHONINSPECT */
} PyConfigAsObjects;
PyInterpreterConfig 구조체는 메인 인터프리터와 서브인터프리터 사이에서 달라질 수 있는 설정을 담습니다. 메인 인터프리터의 경우 이 설정은 Py_InitializeMainInterpreter()에 의해 자동으로 채워집니다.
typedef struct {
PyBoolObject *is_main_interpreter; /* Easily check for subinterpreters */
} PyInterpreterConfig;
이 구조체들은 객체 포인터로만 구성되므로 명시적인 초기화자 정의가 필요하지 않습니다. 구조체 메모리를 0으로 채우는 C99의 기본 초기화로 충분합니다.
메인 인터프리터 초기화 완료하기
초기화 과정의 마지막 단계는 구성 설정을 실제로 적용하고, 메인 인터프리터가 완전히 동작하도록 부트스트래핑을 마치는 것입니다:
int Py_InitializeMainInterpreter(const PyConfigAsObjects *config);
Py_BuildPythonConfig와 마찬가지로, 이 호출은 구성 데이터에서 문제가 발견되면 치명적 오류를 일으키는 대신 예외를 발생시키고 오류 반환을 보고합니다. (추후 확정 예정입니다. 기존 공개 초기화 API는 여러 번 호출되어도 오류 없이 동작하며, 한 번만 쓸 수 있는 설정에 대한 변경은 단순히 무시합니다. 새 API를 이전 API보다 엄격하게 만들려고 하기보다는 그 동작을 유지하는 편이 합리적일 수 있습니다)
모든 구성 설정은 필수입니다. 구성 구조체가 완전히 채워지도록 항상 Py_BuildPythonConfig를 거쳐 전달해야 합니다.
호출이 성공하면 Py_IsInitialized()는 참이 되고 Py_IsInitializing()은 거짓이 됩니다. 코어 런타임만 초기화된 단계의 인터프리터에 대해 앞서 설명한 주의 사항은 더 이상 적용되지 않습니다.
Py_IsInitialized()가 참일 때 Py_InitializeMainInterpreter()를 다시 호출하려고 하는 것은 오류입니다.
그러나 __main__ 모듈과 관련된 일부 메타데이터는 여전히 불완전할 수 있습니다:
sys.argv[0]은 아직 최종 값을 갖지 않았을 수 있습니다- CPython으로 모듈이나 패키지를 실행할 때는
-m이 됩니다 - 유효한
sys.path항목(일반적으로 zip 파일이나 디렉터리)을 실행할 때는__main__모듈의 위치가 아니라sys.path[0]과 같은 값이 됩니다 - 그 외의 경우에는 정확한 값을 갖습니다:
- 일반 스크립트를 실행하는 경우 스크립트 이름입니다
- 제공된 문자열을 실행하는 경우
-c입니다 - 표준 입력에서 실행하는 경우
-또는 빈 문자열입니다
- CPython으로 모듈이나 패키지를 실행할 때는
__main__모듈의 메타데이터는 여전히 이 모듈이 내장 모듈이라고 나타냅니다
이 함수는 일반적으로 마지막 작업으로(Py_IsInitialized()가 이미 설정된 후) site를 암묵적으로 임포트합니다. 구성 설정에서 “enable_site_config” 플래그를 Py_False로 설정하면 이 동작이 비활성화되며, 이후 프로세스에서 import site를 명시적으로 실행하더라도 전역 상태에 대한 부작용이 모두 사라집니다.
메인 모듈 준비하기
Note
PEP 587에서는 PyRun_PrepareMain과 PyRun_ExecMain이
별도로 노출되지 않으며, 대신 main을 준비하고 실행한 다음 Python 인터프리터를 종료하는 Py_RunMain API를 통해 접근합니다.
이 하위 단계는 __main__ 모듈 코드의 실행을 실제로 시작하지 않고 __main__ 모듈 관련 메타데이터를 마저 채웁니다.
이 작업은 다음 API를 호출하여 처리합니다:
int PyRun_PrepareMain();
이 작업은 메인 인터프리터에서만 허용되며, 현재 스레드 상태가 서브인터프리터에 속한 스레드에서 호출하면 RuntimeError를 발생시킵니다.
실제 처리는 구성 구조체의 일부로 인터프리터 상태에 저장된 main 관련 설정에 따라 이루어집니다.
prepare_main이 0이면 이 호출은 아무 작업도 하지 않습니다.
main_source, main_path, main_module, main_stream, main_code가 모두 NULL이면 이 호출은 아무 작업도 하지 않습니다.
main_source, main_path, main_module, main_stream, main_code 중 둘 이상이 설정되면 RuntimeError가 보고됩니다.
main_code가 이미 설정되어 있으면 이 호출은 아무 작업도 하지 않습니다.
main_stream이 설정되어 있고 run_implicit_code도 설정되어 있으면, startup_file에 지정된 파일을 읽고 컴파일하여 __main__ 네임스페이스에서 실행합니다.
main_source, main_path 또는 main_module이 설정되어 있으면, 이 호출은 main_code를 채우는 데 필요한 모든 단계를 수행합니다:
main_source의 경우, 제공된 문자열을 컴파일하여main_code에 저장합니다.main_path의 경우:- 제공된 경로가 유효한
sys.path항목으로 인식되면, 이를sys.path[0]으로 삽입하고main_module을__main__으로 설정한 다음, 아래의main_module과 같은 방식으로 처리를 계속합니다. - 그렇지 않으면, 경로를 CPython 바이트코드 파일로 읽습니다
- 이것이 실패하면 Python 소스 파일로 읽어서 컴파일합니다
- 뒤의 두 경우에는 코드 객체를
main_code에 저장하고__main__.__file__을 적절히 설정합니다
- 제공된 경로가 유효한
main_module의 경우:- 모든 부모 패키지를 임포트합니다
- 모듈의 로더를 결정합니다
- 로더가 해당 모듈이 패키지라고 알려 주면,
main_module의 끝에.__main__을 덧붙여 다시 시도합니다 (마지막 이름 세그먼트가 이미.__main__이면 즉시 실패합니다) - 모듈 소스 코드를 찾으면 컴파일된 모듈 코드를
main_code로 저장하고,__main__의 다음 속성을 적절히 채웁니다:__name__,__loader__,__file__,__cached__,__package__.
(참고: 이 절에서 설명하는 동작은 새로운 것이 아니며, 새 구성 시스템에 맞게 조정한 CPython 인터프리터의 현재 동작을 정리한 것입니다)
메인 모듈 실행
Note
PEP 587에서는 PyRun_PrepareMain과 PyRun_ExecMain이
별도로 노출되지 않습니다. 대신 main을 준비하고 실행한 다음 Python 인터프리터를 종료 처리하는 Py_RunMain API를 통해 접근합니다.
이 하위 단계는 실제 __main__ 모듈 코드의 실행을 다룹니다.
이는 다음 API를 호출하여 처리합니다:
int PyRun_ExecMain();
이 연산은 메인 인터프리터에서만 허용되며, 현재 스레드 상태가 서브인터프리터에 속한 스레드에서 호출하면 RuntimeError를 발생시킵니다.
실제 처리는 구성 구조체의 일부로 인터프리터 상태에 저장된 main 관련 설정에 따라 진행됩니다.
main_stream과 main_code가 모두 NULL이면 이 호출은 아무 작업도 하지 않습니다.
main_stream과 main_code가 모두 설정되어 있으면 RuntimeError가 보고됩니다.
main_stream과 prompt_stream이 모두 설정되어 있으면, main 실행을 새 내부 API에 위임합니다:
int _PyRun_InteractiveMain(PyObject *input, PyObject* output);
main_stream이 설정되어 있고 prompt_stream이 NULL이면, main 실행을 새 내부 API에 위임합니다:
int _PyRun_StreamInMain(PyObject *input);
main_code가 설정되어 있으면, main 실행을 새 내부 API에 위임합니다:
int _PyRun_CodeInMain(PyCodeObject *code);
main 실행이 완료된 후 inspect_main이 설정되어 있거나 PYTHONINSPECT 환경 변수가 설정되어 있으면, PyRun_ExecMain은 _PyRun_InteractiveMain(sys.__stdin__, sys.__stdout__)을 호출합니다.
구성 데이터의 내부 저장
인터프리터 상태 객체가 최소한 PyConfigAsObjects 및 PyInterpreterConfig 구조체의 내장 사본을 포함하도록 확장하여, 초기화 중에 제공된 구성 설정의 세부 정보가 인터프리터 상태에 포함되도록 갱신합니다.
디버깅 목적으로, 구성 설정은 sys._configuration 단순 네임스페이스로 노출됩니다 (sys.flags 및 sys.implementation과 유사합니다. 이 속성들 자체도 두 단계의 설정에 각각 대응하는 단순 네임스페이스입니다:
all_interpretersactive_interpreter
필드 이름은 구성 구조체의 필드 이름과 일치하지만, hash_seed는 의도적으로 제외합니다.
밑줄로 시작하는 속성을 의도적으로 선택했습니다. 이 설정들은 Python 언어 정의의 일부가 아니라 CPython 구현의 일부이기 때문입니다. 표준 라이브러리에서 구현 간 호환성을 지원하기 위해 새로운 설정이 필요하다면, PEP 421에 설명된 대로 다른 구현들과 합의한 뒤 sys.implementation의 새로운 필수 속성으로 노출해야 합니다.
이것들은 초기 구성 설정의 스냅숏입니다. 인터프리터는 런타임 중에 이것들을 수정하지 않습니다(위에서 언급한 경우는 제외).
서브인터프리터 생성 및 구성
새로운 구성 설정은 인터프리터 상태에 저장되므로, 새 서브인터프리터가 생성될 때 초기화해야 합니다. 이는 PyThreadState_Swap(NULL); 때문에 예상보다 까다로운 것으로 밝혀졌습니다(다행히 CPython 자체의 임베딩 테스트가 이 호출을 실행하므로 개발 중에 이 문제를 발견할 수 있었습니다).
이 경우에 대한 간단한 해결책을 제공하기 위해, 이 PEP는 다음과 같은 새 API를 추가할 것을 제안합니다:
Py_InterpreterState *Py_InterpreterState_Main();
이는 Py_InterpreterState_Head()에 대응하는 API로, 가장 최근의 인터프리터 대신 현재 존재하는 인터프리터 중 가장 오래된 인터프리터만 보고합니다. 기존 스레드 상태가 있는 스레드에서 Py_NewInterpreter()를 호출하면, 새 서브인터프리터를 초기화할 때 해당 스레드의 인터프리터 구성을 사용합니다. 현재 스레드 상태가 없으면 Py_InterpreterState_Main()의 구성을 사용합니다.
기존 Py_InterpreterState_Head() API를 대신 사용할 수도 있지만, 그 참조는 서브인터프리터가 생성되고 파괴됨에 따라 바뀌는 반면, PyInterpreterState_Main()은 항상 Py_InitializeRuntime()에서 생성된 초기 인터프리터 상태를 가리킵니다.
임베딩 API에 새로운 제약도 추가됩니다. 서브인터프리터가 아직 존재하는 동안 메인 인터프리터를 삭제하려고 하면 이제 치명적 오류가 발생합니다.
안정 ABI
Python 인터프리터를 임베딩하는 것은 단순히 확장 모듈을 작성하는 것보다 훨씬 높은 수준의 결합을 수반하므로, 이 PEP에서 제안하는 API 대부분은 안정 ABI에서 제외됩니다.
새로 노출되는 API 중 안정 ABI에 포함되는 것은 Py_IsInitializing()과 Py_IsRuntimeInitialized() 조회 함수뿐입니다.
빌드 시점 구성
이 PEP는 빌드 시점 구성 설정의 처리 방식을 변경하지 않으므로, sys.implementation의 내용이나 sysconfig.get_config_vars()의 결과에 아무런 영향을 주지 않습니다.
하위 호환성
하위 호환성은 주로 Py_BuildPythonConfig()가 전역 변수와 환경 변수에 저장된, 기존에 정의된 모든 구성 설정을 조회하고, Py_InitializeMainInterpreter()가 영향을 받는 설정을 관련 위치에 다시 기록하도록 보장함으로써 유지됩니다.
알려진 비호환성 하나는 현재 지연 방식으로 읽히는 일부 환경 변수가 대신 인터프리터 초기화 중에 한 번만 읽힐 수 있다는 점입니다. 참조 구현이 성숙해짐에 따라 이러한 사항은 사례별로 더 자세히 논의할 것입니다. 현재 동적으로 조회되는 것으로 알려진 환경 변수는 다음과 같습니다:
PYTHONCASEOK:os.environ['PYTHONCASEOK']에 값을 쓰더라도 더 이상 임포트 시 인터프리터의 파일 이름 대소문자 차이 처리 방식이 동적으로 바뀌지 않습니다(미확정)PYTHONINSPECT:os.environ['PYTHONINSPECT']는__main__모듈의 실행이 끝난 후에도 여전히 검사됩니다
Py_Initialize() 방식의 초기화는 계속 지원됩니다. 이 방식은 내부적으로 새 API(의 적어도 일부 요소)를 사용하지만, 현재와 동일한 동작을 계속 유지하여 이후 PySys_SetArgv 호출이 있을 때까지 sys.argv가 채워지지 않도록 보장합니다(미확정). 현재 Py_Initialize() 이전에 호출할 수 있는 모든 API는 계속 그렇게 호출할 수 있으며, Py_InitializeRuntime() 이전에 호출하는 것도 지원합니다.
시스템 Python 실행 파일
시스템 관리자 권한으로 시스템 유틸리티를 실행할 때에는 CPython의 기본 동작 중 상당수가 바람직하지 않습니다. 신뢰할 수 없는 코드가 상승된 권한으로 실행될 수 있기 때문입니다. 가장 문제가 되는 부분은 사용자 site 디렉터리가 활성화되어 있고, 환경 변수를 신뢰하며, 실행된 파일이 들어 있는 디렉터리가 임포트 경로의 맨 앞에 놓인다는 점입니다.
이슈 16499 [6]에서 일반 CPython 실행 파일의 동작을 바꾸는 -I 옵션을 추가했지만, 이는 찾아내기 어려운 해결책입니다(게다가 이미 복잡한 CLI에 옵션을 또 하나 추가합니다). 이 PEP는 그 대신 별도의 system-python 실행 파일을 추가할 것을 제안합니다
현재로서는 기본 동작이 다른 별도의 실행 파일을 제공하면 유지 보수하기가 지나치게 어려울 것입니다. 이 PEP의 목표 중 하나는 유지 보수하기 어려운 부트스트래핑 코드의 상당 부분을 보다 일반적인 CPython 코드로 대체할 수 있게 하고, 별도의 애플리케이션이 Py_Main의 핵심 구성 요소를 더 쉽게 활용할 수 있게 하는 것입니다. 이 변경 사항을 PEP에 포함한 것은 이론상으로는 좋아 보이지만 실제로는 문제가 있는 것으로 드러나는 설계가 채택되는 일을 피하기 위해서입니다.
이러한 종류의 “대체 CLI”를 깔끔하게 지원하는 것이, CPython이 지원하는 여러 실행 모드 중 어느 것을 사용할지 결정하는 핵심 로직을 더 잘 노출하도록 변경을 제안하는 주된 이유입니다:
- 스크립트 실행
- 디렉터리/zip 파일 실행
- 명령 실행(“-c” 스위치)
- 모듈 또는 패키지 실행(“-m” 스위치)
- stdin으로부터의 실행(비대화형)
- 대화형 stdin
실제로 이를 구현하다 보면 초기화 단계에서 사용할 더 나은 인자 파싱 인프라가 필요하다는 점이 드러날 수도 있습니다.
미해결 질문
Py_BuildPythonConfig및Py_InitializeMainInterpreter의 오류 세부 사항(구현이 진행됨에 따라 더 명확해질 것입니다)
구현
참조 구현은 CPython 참조 인터프리터 내부에서 비공개 API 리팩터링으로 개발되고 있습니다(독립 프로젝트로 유지하려는 시도는 비현실적인 것으로 드러났기 때문입니다).
PEP 587은 이 제안 중 Python 3.8의 공개 API로 제안할 만큼 충분히 안정적이라고 여겨지는 일부를 추려 냅니다.
현황(Python 3.6 기준)
인터프리터를 구성하는 현재의 메커니즘은 지난 20여 년 동안 상당히 임시방편적으로 쌓여 왔으며, 그 결과 문서화 수준도 제각각인 다소 일관성 없는 인터페이스가 되었습니다.
기존 설정과 그 처리 방식에 대한 추가 논의는 PEP 587도 참조하십시오.
(참고: 아래 정보 중 일부는 정리하여 3.x용 C API 문서에 추가하는 편이 좋을 것입니다. 모두 CPython에 한정된 내용이므로 언어 레퍼런스에는 어울리지 않습니다)
환경 변수 무시하기
-E 명령줄 옵션을 사용하면 Python 인터프리터를 초기화할 때 모든 환경 변수를 무시할 수 있습니다. 임베딩 애플리케이션은 Py_Initialize()를 호출하기 전에 Py_IgnoreEnvironmentFlag를 설정하여 이 동작을 활성화할 수 있습니다.
CPython 소스 코드에서 Py_GETENV 매크로는 이 플래그를 암묵적으로 검사하며, 플래그가 설정되어 있으면 항상 NULL을 반환합니다.
<TBD: PYTHONCASEOK는 이 설정과 관계없이 검사되는 것으로 알고 있습니다 > <TBD: -E는 Windows 레지스트리 키도 무시합니까? >
무작위 해싱
무작위 해싱은 -R 명령줄 옵션(3.3 이전 릴리스)과 PYTHONHASHSEED 환경 변수를 통해 제어됩니다.
Python 3.3에서는 환경 변수만 여전히 의미가 있습니다. 이 환경 변수는 무작위 해싱을 비활성화하거나(시드 값 0을 사용하여) 특정 해시 값을 강제하는 데(예: 테스트의 재현성을 위해, 또는 프로세스 간에 해시 값을 공유하기 위해) 사용할 수 있습니다
그러나 임베딩 애플리케이션은 해시 무작위화를 명시적으로 요청하려면 Py_HashRandomizationFlag를 사용해야 합니다(CPython은 이 플래그를 Py_Initialize()가 아닌 Py_Main()에서 설정합니다).
새 구성 API를 사용하면 임베딩 애플리케이션이 다른 수단(예: 구성 파일이나 별도의 환경 변수)으로 제공되는 텍스트 기반 구성 설정에 PYTHONHASHSEED 처리를 손쉽게 재사용할 수 있어야 합니다.
Python과 표준 라이브러리의 위치 찾기
Python 바이너리와 표준 라이브러리의 위치는 여러 요소의 영향을 받습니다. 이 계산에 사용되는 알고리즘은 소스 코드 [3], [4] 외에는 어디에도 문서화되어 있지 않습니다. 그 설명조차도 불완전합니다. Python 3.3에서 추가된 가상 환경 지원(PEP 405에 자세히 설명되어 있습니다)이 반영되지 않았기 때문입니다.
이러한 계산은 다음 함수 호출(Py_Initialize() 호출 전에 이루어지는)과 환경 변수의 영향을 받습니다:
Py_SetProgramName()Py_SetPythonHome()PYTHONHOME
또한 파일 시스템에서 pyvenv.cfg 파일(PEP 405 참조)이 있는지 검사하며, 없으면 lib/os.py (Windows) 또는 lib/python$VERSION/os.py 파일이 있는지 검사합니다.
PREFIX와 EXEC_PREFIX에 대한 빌드 시점 설정도 관련이 있으며, Windows의 일부 레지스트리 설정도 마찬가지입니다. 하드코딩된 대체값은 소스 체크아웃에서 작업할 때의 CPython 소스 트리 및 빌드 출력의 배치를 기반으로 합니다.
sys.path 구성하기
임베딩 애플리케이션은 Py_Initialize() 전에 Py_SetPath()를 호출하여 sys.path 계산을 완전히 재정의할 수 있습니다. 계산의 일부만 허용하는 것은 간단하지 않은데, 초기화가 이미 완료된 후에 sys.path를 수정하면 시작 과정에서 표준 라이브러리 모듈을 임포트할 때 그 수정 사항이 적용되지 않기 때문입니다.
Py_GetPath()의 첫 호출(Py_Initialize()에서 암묵적으로 이루어짐) 전에 Py_SetPath()가 사용되지 않으면, 위의 위치 데이터 계산을 바탕으로 PYTHONPATH 환경 변수와 함께 적절한 경로 항목을 계산합니다.
<TBD: Windows에서는 레지스트리와 관련된 여러 작업도 있습니다>
시작 시 암묵적으로 임포트되는(-S 옵션으로 비활성화하지 않는 한) site 모듈은 해당 문서 [5]에 설명된 대로 이 초기 경로 집합에 추가 경로를 더합니다.
-s 명령줄 옵션을 사용하면 추가되는 디렉터리 목록에서 사용자 사이트 디렉터리를 제외할 수 있습니다. 임베딩 애플리케이션은 Py_NoUserSiteDirectory 전역 변수를 설정하여 이를 제어할 수 있습니다.
특정 시스템에서 특정 Python 실행 파일의 기본 경로 설정을 확인하려면 다음 명령을 사용할 수 있습니다:
./python -c "import sys, pprint; pprint.pprint(sys.path)"- 표준 구성./python -s -c "import sys, pprint; pprint.pprint(sys.path)"- 사용자 사이트 디렉터리 비활성화./python -S -c "import sys, pprint; pprint.pprint(sys.path)"- 모든 사이트 경로 수정 비활성화
(참고: -c 대신 -m site를 사용해도 비슷한 정보를 볼 수 있지만, 이 방법은 모든 경로 항목에 os.abspath를 호출하여 상대 경로 항목을 절대 경로처럼 보이게 하므로 다소 오해의 소지가 있습니다. 또한 site 모듈을 사용하면 마지막 경우에 문제가 생깁니다. Python 3.3 이전 버전에서는 site를 명시적으로 임포트하면 -S가 피하려는 경로 수정이 수행되고, 3.3 이상에서는 현재 -m site와 -S를 함께 사용하면 실패하기 때문입니다)
sys.path[0]의 계산은 비교적 간단합니다:
- 일반 스크립트(Python 소스 또는 컴파일된 바이트코드)의 경우
sys.path[0]은 스크립트가 들어 있는 디렉터리가 됩니다. - 유효한
sys.path항목(일반적으로 zip 파일이나 디렉터리)의 경우,sys.path[0]은 그 경로가 됩니다 - 대화형 세션이거나, stdin에서 실행하거나,
-c또는-m스위치를 사용하는 경우sys.path[0]은 빈 문자열이 되며, 임포트 시스템은 이를 현재 디렉터리에서 임포트할 수 있다는 뜻으로 해석합니다
sys.argv 구성하기
이 PEP에서 다루는 다른 대부분의 설정과 달리 sys.argv는 Py_Initialize()에 의해 암묵적으로 설정되지 않습니다. 대신 Py_SetArgv()를 명시적으로 호출하여 설정해야 합니다.
CPython은 Py_Main()에서 Py_Initialize()를 호출한 후에 이를 호출합니다. sys.argv[1:]의 계산은 간단합니다. 스크립트 이름 뒤에, 또는 -c나 -m 옵션의 인자 뒤에 전달된 명령줄 인자들입니다.
sys.argv[0]의 계산은 조금 더 복잡합니다:
- 일반 스크립트(소스 또는 바이트코드)의 경우 스크립트 이름이 됩니다
sys.path항목(일반적으로 zip 파일 또는 디렉터리)의 경우 처음에는 zip 파일 또는 디렉터리 이름이 되지만, 나중에runpy모듈에 의해 임포트된__main__모듈의 전체 경로로 변경됩니다.-m스위치로 지정된 모듈의 경우 처음에는 문자열"-m"이 되지만, 나중에runpy모듈에 의해 실행된 모듈의 전체 경로로 변경됩니다.-m스위치로 지정한 패키지의 경우 처음에는 문자열"-m"이지만, 나중에runpy모듈이 이 값을 실행된 패키지의__main__하위 모듈의 전체 경로로 바꿉니다.-c로 실행된 명령의 경우 문자열"-c"가 됩니다- stdin으로부터의 입력을 명시적으로 요청한 경우 문자열
"-"가 됩니다 - 그 밖의 경우에는 빈 문자열이 됩니다
임베딩 애플리케이션은 Py_SetArgv를 직접 호출해야 합니다. 이를 수행하는 CPython 로직은 Py_Main()의 일부이며 별도로 노출되지 않습니다. 하지만 runpy 모듈은 runpy.run_module과 runpy.run_path에서 대략 동등한 로직을 제공합니다.
기타 구성 설정
TBD: 다음 항목의 초기화를 더 자세히 다룰 예정입니다:
- 임포트 시스템을 완전히 비활성화하기
- 초기 경고 시스템 상태:
sys.warnoptions- (-W 옵션, PYTHONWARNINGS)
- 임의의 확장 옵션(예:
faulthandler를 자동으로 활성화하기 위한 옵션):sys._xoptions- (-X 옵션)
- 다음에서 사용하는 파일 시스템 인코딩:
sys.getfsencodingos.fsencodeos.fsdecode
- 다음에서 사용하는 IO 인코딩과 버퍼링:
sys.stdinsys.stdoutsys.stderr- (-u 옵션, PYTHONIOENCODING, PYTHONUNBUFFEREDIO)
- 바이트코드 파일을 암묵적으로 캐시할지 여부:
sys.dont_write_bytecode- (-B 옵션, PYTHONDONTWRITEBYTECODE)
- 대소문자를 구분하지 않는 플랫폼에서 파일 이름의 대소문자를 정확히 일치시키도록 강제할지 여부
os.environ["PYTHONCASEOK"]
sys.flags에서 Python 코드에 노출되는 그 밖의 설정은 다음과 같습니다:debug(pgen 파서의 디버깅 출력 활성화)inspect(__main__ 종료 후 대화형 인터프리터 진입)interactive(stdin을 tty로 취급)optimize(__debug__ 상태, .pyc 또는 .pyo 작성, 독스트링 문자열 제거)no_user_site(사용자 site 디렉터리를 sys.path에 추가하지 않음)no_site(시작 시 site를 암묵적으로 임포트하지 않음)ignore_environment(구성 중 환경 변수 사용 여부)verbose(온갖 종류의 잡다한 출력 활성화)bytes_warning(암묵적인 str/bytes 상호 작용에 대한 경고/오류)quiet(verbose가 함께 활성화되어 있거나, stdin이 tty이고 인터프리터가 대화형 모드로 실행된 경우에도 배너 출력 비활성화)
- CPython의 시그널 핸들러를 설치할지 여부
현재 CPython 구성의 상당 부분은 C 수준 전역 변수를 통해 처리됩니다:
Py_BytesWarningFlag (-b)
Py_DebugFlag (-d option)
Py_InspectFlag (-i option, PYTHONINSPECT)
Py_InteractiveFlag (property of stdin, cannot be overridden)
Py_OptimizeFlag (-O option, PYTHONOPTIMIZE)
Py_DontWriteBytecodeFlag (-B option, PYTHONDONTWRITEBYTECODE)
Py_NoUserSiteDirectory (-s option, PYTHONNOUSERSITE)
Py_NoSiteFlag (-S option)
Py_UnbufferedStdioFlag (-u, PYTHONUNBUFFEREDIO)
Py_VerboseFlag (-v option, PYTHONVERBOSE)
위 변수들의 경우 명령줄 옵션과 환경 변수를 C 전역 변수로 변환하는 작업은 Py_Main이 처리하므로, 각 임베딩 애플리케이션은 기본값을 변경하려면 해당 변수들을 적절히 설정해야 합니다.
일부 구성은 OS 수준 환경 변수로만 제공할 수 있습니다:
PYTHONSTARTUP
PYTHONCASEOK
PYTHONIOENCODING
Py_InitializeEx() API는 CPython의 시그널 핸들러를 설치할지 여부를 나타내는 불리언 플래그도 받습니다.
마지막으로, 일부 대화형 동작(예: 소개 배너 출력)은 운영 체제가 표준 입력을 터미널 연결로 보고하는 경우에만 실행됩니다.
TBD: “-x” 옵션이 처리되는 방식(메인 스크립트의 첫 번째 주석 줄 처리를 건너뜀)을 문서화해야 합니다
자세한 작업 순서에 대한 메모는 [1]도 참고하십시오.
참고 문헌
Copyright
This document has been placed in the public domain.