나도 CPython 파헤치기3

구성과 입력

👏 원리 설명

코드가 실행 가능한 단계 까지 도달하기의 과정 중 인터프리터가 파이썬을 실행 하는 명령어를 살펴 보자면, 이렇게 처리 할 수 있다

  1. python3 -c
  2. python3 -m
  3. python3 { file }
  4. cat { file } | python3
  5. repl …

이렇게 실행 하는 인터프리터는 아래와 같은 세 가지 요소가 필요하다.

  1. 실행 할 모듈
  2. 변수 등을 저장할 상태
  3. 활성화 된 옵션의 구성

CPython code guide: PEP7

이렇게 위와 같은 코드 컨밴션이 존재 하지만 PEP7은 8과 달리 검사 도구가 많지 않다. 그렇기 때문에 코어 개발자가 PEP7을 준수 하는지 매번 확인 해야 한다. 그 중 유일한 자동화 검사 도구 중 smell 을 사용 하여 위에 명시 된 또는, 더 많은 코드 가이드를 검사 할 수 있다.

👏 원리 설명

5.1 구성 상태


코드를 실행 하기 전 CPython 런타임은 사용자 옵션과 구성을 설정한다.

  1. PyPreConfig 딕셔너리 초기화 구성
  2. PyConfig 런타임 구성
  3. CPython 인터프리터에 같이 컴파일된 구성

👉 PyPreConfig, PyConfig 구조체는 Include > cpython > initcofig.h 에서 정의함

5.1.1 딕셔너리 초기화 구성

딕셔너리 초기화 구성은 사용자 환경 또는 운영체제와 관련된 구성이기 때문 에 런타임 구성과 구분된다.

PyPreConfig 의 주요 세 가지 기능

PyPreConfig 구조체

PyPreConfig와 연관된 소스 파일 목록

5.1.3 런타임 구성 구조체

딕셔너리 초기화 구성 다음 단계로 PyConfig 런타임 구성 구조체는 다음과 같은 값들을 포함한다.

런타임 구성 데이터는 CPython 런타임 기능의 활성화 여부를 결정한다.

5.1.4 명령줄로 런타임 구성 설정하기

./python.exe -v -c "print('hello world')"

import _frozen_importlib # frozen
import _imp # builtin
import '_thread' # <class '_frozen_importlib.BuiltinImporter'>
import '_warnings' # <class '_frozen_importlib.BuiltinImporter'>
import '_weakref' # <class '_frozen_importlib.BuiltinImporter'>
import '_io' # <class '_frozen_importlib.BuiltinImporter'>
import 'marshal' # <class '_frozen_importlib.BuiltinImporter'>
import 'posix' # <class '_frozen_importlib.BuiltinImporter'>
import '_frozen_importlib_external' # <class '_frozen_importlib.FrozenImporter'>
# installing zipimport hook
...
Python 3.9.17+ (heads/3.9-dirty:38b489bae8, Aug 12 2023, 17:34:53) 
[Clang 14.0.3 (clang-1403.0.22.14.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
hello world

상세모드 설정에 대한 우선순위

  1. config > verbose: 기본값이 -1로 소스코드에 하드 코딩 되어있음
  2. PYTHONVERBOSE 환경 변수를 config -> verbose를 설정하는 데 사용
  3. 환경 변수가 없으면 기본 값인 -1을 사용
  4. Python > initconfig.c 의 config_parse_cmdline()은 명시된 명령줄 플래그를 사용 해 모드를 설정함
  5. _Py_GetGlobalVaribalesAsDict()가 값을 전역 변수 Py_VerboseFlag로 복사

이렇게, 모든 PyConfig 값에는 같은 순서와 우선순위가 적용된다.

5.1.5 런타임 플래그 확인하기

CPython 인터프리터의 런타임 플래그는 CPython의 동작들을 끄고 켜는 데 사용하는 고급 기능이다. 파이썬 세션 중에 sys.flags 네임드 튜플로 상세 모드나 메시지 없는 모드 같은 런타임 플래그에 접근이 가능하다.

👏 원리 설명

5.2 빌드 구성


빌드 구성은 최상위 폴더인 pyconfig.h에 정의한다. 이 파일은 macOS, Linux용 빌드 과정 중 ./configure 단계나 왼도우의 build.bat 실행 중에 자동으로 생성 된다.

빌드 구성 확인

./python.exe -m sysconfig

빌드 구성 항목들은 컴파일 시에 결정 되는 값들로 바이너리에 링크 할 추가 모듈 선택에 사용됨.

예를 들어, 디버거와 계측(instrucmentation) 라이브러리, 메모리 할당자는 모두 컴파일 시 결정된다.

5.3 입력에서 모듈 만들기


코드를 실행하려면 먼저 입력을 모듈로 컴파일 해야 한다.

입력 방식

이렇게 읽어 들인 입력은 파서를 거쳐 컴파일러로 전달된다.

이렇게 유연한 입력 방식을 제공하기 위해 CPython은 CPython 소스 코드의 상당 분량을
파서의 입력 처리에 사용한다.

연관된 소스 파일 목록

입력과 파일 읽기

  1. CPython은 런타임 구성과 명령줄 인자가 준비되면 실행 할 코드를 불러온다.
  1. 그 다음 CPython은 불러온 코드를 새로 생성 된 Py?Config 인스턴스에 설정된 옵션들과 함께 실행된다.

명령줄 문자열 입력

CPython은 -c 옵션을 사용 해 명령줄에서 작은 파이썬 어플리케이션을 실행 할 수 있다.

마치 이렇게 ./python.exe -c "print(2 ** 2)"

문자열을 입력 받아 실행하는 과정

  1. Modules>main.c>pymain_run_command()가 실행 되며 -c로 전달 된 명령은 C의 wchar_t*타입 인자로 함수에 전달된다.

    • wchar_t* 타입은 UTF-8 문자를 저장 할 수 있기 때문에 CPython에서 저수준 유니코드 데이터를 저장하는 타입으로 사용된다.
  2. pymain_run_command()는 파이썬 바이트열 객체를 PyRun_SimpleStringFlags()로 넘겨 실행

    • PyRun_SimpleStringFlags()는문자열을 파이썬 모듈로 변환 하고 실행
    • 파이썬 모듈을 독립된 모듈로 실행 하려면 __main__ 진입점이 필요하기 때문에 PyRun_SimpleStringFlags() 가 진입점을 자동으로 추가함
    • PyRun_SimpleStringFlags()는 딕셔너리와 모듈을 만든 후 PyRun_StringFlags()를 호출 함
    • PyRun_SimpleStringFlags()는 가짜 파일 이름을 만들고 파이썬 파서를 실행 하여 문자열에서 추상 구문 트리(abstract syntax tree, AST)를 생성 해 모듈로 반환함.

-m 플래그는 모듈 패키지의 진입점(__main__)을 실행한다. 이 때 해당 모듈은 sys.path에서 검색 하는데 이 때 임포트 라이브러리의 검색 매너키즘 덕분에 특정 모듈의 파일 시스템 위치를 기억 할 필요는 없다.

runpy 모듈은 Lib > runpy.py 에 위치한 순수한 파이썬 모듈이다. python -m <module> 을 실행 하는 것은 python -m runpy <module> 을 실행 하는 것과 같다. 이렇게, runpy 모듈은 운영체제에서 모듈을 찾아 실행하는 프로세스를 추상화 한다.

runpy의 모듈 실행 과정

  1. 제공된 모듈 이름을 __import__()로 임포트한다.
  2. __name__(모듈 이름)을 __main__이름 공간에 설정한다.
  3. __main__ 이름 공간에서 모듈을 실행한다.
if __name__ == "__main__":
    pass

python test.py 처럼 첫 번째 인자가 파일명이라면 CPython은 파일 핸들을 열어 Python > pythonrun.c 의 PyRun_SimpleFileExFlags()로 핸들을 넘긴다. 이 함수는 세 종류의 파일 경로를 처리 할 수 있다.

  1. .pyc 파일 경로면 run_pyc_file()을 호출한다.
  2. 스크립트 파일(.py) 경로면 PyRun_FileExFlags()를 호출한다.
  3. command | python처럼 파일 경로가 stdin이면 stdin을 파일 핸들로 취급 하고 PyRun_FileExFlags()를 호출한다.

PyRun_FileExFlags()는 파일에서 파이썬 모듈을 생성 하고 run_mod()로 보내 실행한다.

CPython 컴파일러는 스크립트가 호출 될 때 마다 파싱하는 대신, 디스크의 코드 객체 구조체에 컴파일 한 코드를 캐시한다. 그렇기 때문에, .pyc 라는 파일이 컴파일러에 의해 생성 되었다면 파이썬 코드가 수정 되지 않는 이상 빠르게 실행 시킬 수 있는 것이다.

요약


파이썬이 다양한 구성 옵션을 로딩 하는 방식과 코드를 인터프리터에 입력하는 방식들에 대해 공부했다. 파이썬은 이렇게 유연한 입력 방식 덕분에 다양한 어플리케이션에 적합한 언어이다.