반응형

CPython에서는 다양한 방식으로 파이썬 코드를 실행할 수 있음

  1. python -c로 파이썬 문자열을 통해 코드 실행
$ ./python.exe -c "print(3 + 5)"
> 8
  1. python -m으로 모듈 실행하기
  2. python <file>로 파이썬 코드가 들어 있는 파일(경로 명시) 실행하기
  3. cat <file> | python처럼 파이썬 코드를 stdin으로 python에 파이프하기
  4. REPL에서 한번에 하나씩 명령 실행
  5. C API를 이용해 파이썬을 임베디드 환경으로 사용하기

 

인터프리터가 파이썬 코드를 실행하려면 세가지 요소가 필요

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

출처: CPython 파헤치기 5장

 

5.1 구성 상태

  • 파이썬 코드를 실행하기 전 CPython 런타임은 먼저 사용자 옵션과 구성을 설정
  • CPython 구성은 세 부분으로 나뉨 (PEP587)
  1. PyPreConfig 딕셔너리 초기화 구성
  2. PyConfig 런타임 구성
  3. CPython 인터프리터에 같이 컴파일된 구성

PyPreConfig와 PyConfig 구조체는 Include>cpython>initconfig.h에서 정의

더보기
/* --- PyPreConfig ----------------------------------------------- */

typedef struct {
    int _config_init;     /* _PyConfigInitEnum value */

    /* Parse Py_PreInitializeFromBytesArgs() arguments?
       See PyConfig.parse_argv */
    int parse_argv;

    /* If greater than 0, enable isolated mode: sys.path contains
       neither the script's directory nor the user's site-packages directory.

       Set to 1 by the -I command line option. If set to -1 (default), inherit
       Py_IsolatedFlag value. */
    int isolated;

    /* If greater than 0: use environment variables.
       Set to 0 by -E command line option. If set to -1 (default), it is
       set to !Py_IgnoreEnvironmentFlag. */
    int use_environment;

    /* Set the LC_CTYPE locale to the user preferred locale? If equals to 0,
       set coerce_c_locale and coerce_c_locale_warn to 0. */
    int configure_locale;

    /* Coerce the LC_CTYPE locale if it's equal to "C"? (PEP 538)

       Set to 0 by PYTHONCOERCECLOCALE=0. Set to 1 by PYTHONCOERCECLOCALE=1.
       Set to 2 if the user preferred LC_CTYPE locale is "C".

       If it is equal to 1, LC_CTYPE locale is read to decide if it should be
       coerced or not (ex: PYTHONCOERCECLOCALE=1). Internally, it is set to 2
       if the LC_CTYPE locale must be coerced.

       Disable by default (set to 0). Set it to -1 to let Python decide if it
       should be enabled or not. */
    int coerce_c_locale;

    /* Emit a warning if the LC_CTYPE locale is coerced?

       Set to 1 by PYTHONCOERCECLOCALE=warn.

       Disable by default (set to 0). Set it to -1 to let Python decide if it
       should be enabled or not. */
    int coerce_c_locale_warn;

#ifdef MS_WINDOWS
    /* If greater than 1, use the "mbcs" encoding instead of the UTF-8
       encoding for the filesystem encoding.

       Set to 1 if the PYTHONLEGACYWINDOWSFSENCODING environment variable is
       set to a non-empty string. If set to -1 (default), inherit
       Py_LegacyWindowsFSEncodingFlag value.

       See PEP 529 for more details. */
    int legacy_windows_fs_encoding;
#endif

    /* Enable UTF-8 mode? (PEP 540)

       Disabled by default (equals to 0).

       Set to 1 by "-X utf8" and "-X utf8=1" command line options.
       Set to 1 by PYTHONUTF8=1 environment variable.

       Set to 0 by "-X utf8=0" and PYTHONUTF8=0.

       If equals to -1, it is set to 1 if the LC_CTYPE locale is "C" or
       "POSIX", otherwise it is set to 0. Inherit Py_UTF8Mode value value. */
    int utf8_mode;

    /* If non-zero, enable the Python Development Mode.

       Set to 1 by the -X dev command line option. Set by the PYTHONDEVMODE
       environment variable. */
    int dev_mode;

    /* Memory allocator: PYTHONMALLOC env var.
       See PyMemAllocatorName for valid values. */
    int allocator;
} PyPreConfig;

 

5.1.1 딕셔너리 초기화 구성

  • 사용자 환경 또는 운영 체제와 관련된 구성이기 때문에 런타임 구성과 구분됨

PyPreConfig 의 세가지 주요 기능

  1. 파이썬 메모리 할당자 설정
  2. LC_CTYPE 로캘(locale)을 시스템 또는 사용자 선호 로캘로 구성하기
  3. UTF-8 모드 설정하기(PEP540)

아래와 같은 int 타입 필드들을 포함

  1. allocator: PYMEM_ALLOCATOR_MALLOC 같은 값을 사용해 메모리 할당자를 선택
  2. configure_locale: LC_CTYPE 로캘을 사용자 선호 로캘로 설정. 0으로 설정하면 coerce_c_locale과 coerce_c_locale_warn을 0으로 설정
  3. coerce_c_locale: 2로 설정하면 C 로캘을 강제로 적용. 1로 설정하면 LC_CTYPE을 읽은 후 강제로 적용할지 결정
  4. coerce_c_locale_warn: 0이 아니면 C로캘이 강제로 적용될 때 경고가 발생
  5. dev_mode: 개발 모드를 활성화
  6. isolated: 격리 모드를 활성화. sys.path에 스크립트 디렉토리와 사용자의 사이트 패키지 디렉토리가 포함되지 않음
  7. legacy_windows_fs_encoding: 0이 아니면 UTF-8 모드를 비활성화하고 파이썬 파일 시스템 인코딩을 mbcs로 설정(윈도우 전용)
  8. parse_argv: 0이 아니면 명령줄 인자를 사용
  9. use_environment: 0보다 큰 값이면 환경 변수를 사용
  10. utf8_mode_: 0이 아니면 UTF-8모드를 활성화

 

5.1.2 연관된 소스 파일 목록

PyPreConfig와 연관된 소스 파일 목록

  1. Python/initconfig.c : 시스템 환경에서 불러온 구성을 명령줄 플래그와 결합
  2. Include/cpython/initconfig.h : 초기화 구성 구조체를 정의

5.1.3 런타임 구성 구조체

PyConfig 런타임 구성 구조체는 아래 값들을 포함

  • ‘디버그’나 ‘최적화’같은 실행 모드 플래그
  • 스크립트 파일이나 stdin, 모듈 등 실행 모드
  • -X <option>으로 설정 가능한 확장 옵션
  • 런타임 설정을 위한 환경 변수

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

 

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

  • 파이썬은 다양한 명령줄 인터페이스 옵션을 제공하는데 예로 상세 모드(verbose) 가 있고 주로 개발자 대상으로 활용됨
  • -v 플래그로 상세모드를 활성화하면 파이썬은 모듈을 로딩할 때마다 화면에 메시지를 출력
$ ./python.exe -v -c "print('hello world')"
...
# installing zipimport hook
import 'time' # <class '_frozen_importlib.BuiltinImporter'>
import 'zipimport' # <class '_frozen_importlib.FrozenImporter'>
# installed zipimport hook
  • 사용자 site-packages 디렉토리에 설치된 모듈들과 시스템 호나경의 모든 항목을 임포트하면 수백줄이 출력됨
  • 아래는 상세 모드 설정에 대한 우선순위
  1. config→verbose의 기본값은 -1로 소스 코드에 하드코딩되어 있음
  2. PYTHONVERBOSE 환경 변수를 config→verbose를 설정하는데 사용
  3. 환경 변수가 없으면 기본값인 -1을 사용
  4. Python/initconfig.c의 config_parse_cmdline()은 명시된 명령중 플래그를 사용해 모드를 설정
  5. _Py_GetGlobalVariablesAsDict() 가 값을 전역 변수 Py_VerboseFlag로 복사

 

모든 Pyconfig값에는 같은 순서와 우선순위가 적용됨

출처: CPython 파헤치기 5장

 

5.1.5 런타임 플래그 확인하기

  • Cpython 인터프리터의 런타임 플래그는 CPython의 동작들을 끄고 켜는데 사용하는 고급 기능
  • X 옵션을 이용해 다양한 기능을 활성화할 수 있음
  • 파이썬 세션 중에 sys.flags 네임드 튜플로 상세 모드나 메시지 없는(quite)모드 같은 런타임 플래그에 접근할 수 있음
  • sys._xoptions 딕셔너리에 모든 -X 플래그를 확인할 수 있음

 

-X dev -X utf8 플래그 사용: 기본 인코딩으로 utf-8로 설정

./python.exe -X dev -X utf8

Python 3.9.19+ (heads/3.9:a04a0f6585, Mar 25 2024, 08:21:31)
[Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys; sys.flags
sys.flags(debug=0, inspect=0, interactive=0, optimize=0, dont_write_bytecode=0, no_user_site=0, no_site=0, ignore_environment=0, verbose=0, bytes_warning=0, quiet=0, hash_randomization=1, isolated=0, dev_mode=True, utf8_mode=1, int_max_str_digits=-1)

 

5.2 빌드 구성

  • 런타임 구성을 Include/cpython/initconfig.h에서 정의하듯이 빌드 구성은 최상의 폴더의 pyconfig.h에서 정의
  • 이 파일은 macOS나 리눅스용 빌드 과정중 ./configure 단계에서 자동으로 생성됨
  • 다음 명령으로 빌드 구성을 확인할 수 있음
$ ./python.exe -m sysconfig
Platform: "macosx-14.5-arm64"
Python version: "3.9"
Current installation scheme: "posix_prefix"

Paths: 
  data = "/usr/local"
  include = "/Users/woo-seongchoi/Desktop/SE/cpython-internals/cpython/Include"
  platinclude = "/Users/woo-seongchoi/Desktop/SE/cpython-internals/cpython"
...
  • 빌드 구성항목들은 컴파일 시에 결졍되는 값으로 바이너리에 링크할 추가 모듈 선택애 사용됨
  • 예를 들어 디버거나 계측(instrumentation) 라이브러리, 메모리 할당자는 모두 컴파일 시 결정됨
  • 세 단계의 구성(Build configuration, PyPreconfig, PyConfig)을 모두 완료하면 CPython 인터프리터는 입력된 텍스트를 코드로 실행할 수 있음

5.3 입력에서 모듈 만들기

코드를 실행하려면 먼저 입력을 모듈로 컴파일해야 함. 입력 방식은 아래와 같이 여러가지가 있음

  • 로컬 파일과 패키지
  • 메모리 파이프나 stdin 같은 I/O 스트림
  • 문자열

읽어 들인 입력은 파서를 거쳐 컴파일러에 전달됨

유연한 입력 방식을 제공하기 위해 CPython은 소스 코드의 상당 부분을 파서의 입력 처리에 사용됨

 

5.3.1 연관된 소스 파일 목록

  • Lib > runpy.py : 파이썬 모듈을 임포트 하고 실행하는 표준 라이브러리 모듈
  • Modules > main.c : 파일이나 모듈, 입력 스트림 같은 외부 코드 실행을 감싸는 함수
  • Programs > python.c : 윈도우나, 리눅스, macOS에서 Python의 진입점. 위의 main.c 를 감싸는 역할만 맡음
더보기
/* Minimal main program -- everything is loaded from the library */

#include "Python.h"

#ifdef MS_WINDOWS
int
wmain(int argc, wchar_t **argv)
{
    return Py_Main(argc, argv);
}
#else
int
main(int argc, char **argv)
{
    return Py_BytesMain(argc, argv);
}
#endif
  • Python > pythonrun.c : 명령줄 입력을 처리하는 내부 C API를 감싸는 함수

5.3.2 입력과 파일 읽기

  • CPython은 런타임 구성과 명령줄 인자가 준비디ㅗ면 실행할 코드를 불러옴
  • 이 작업은 modules/main.c의 pymain_main()이 실행

main.c에서 pymain_main()

static int
pymain_main(_PyArgv *args)
{
    PyStatus status = pymain_init(args);
    if (_PyStatus_IS_EXIT(status)) {
        pymain_free();
        return status.exitcode;
    }
    if (_PyStatus_EXCEPTION(status)) {
        pymain_exit_error(status);
    }

    return Py_RunMain();
}
  • 다음으로 CPython은 불러온 코드를 새로 생성된 PyConfig 인스턴스에 설정된 옵션들과 함께 실행함

 

5.3.3 명령줄 문자열 입력

  • -c 옵션을 사용해 명령줄에서 작은 파이썬 애플리케이션을 실행할 수 있음 (ex) print(2 ** 2))
❯ ./python.exe -c "print(2 ** 2)"        
4

 

  • Modules/main.c에서 pymain_run_command()가 실행되며 -c로 전달된 명령은 C의 wchar_t* 타입 인자로 함수에 전달됨
    • wchar_t* 타입은 UTF-8 문자를 저장할 수 있기 때문에 CPython에서 저수준 유니코드 데이터를 저장하는 타입으로 사용됨
    • Objects/unicodeobject.c의 헬퍼 함수 PyUnicode_FromWideChar()를 이용해 wchar_t*를 파이썬 유니코드 문자열로 변환할 수 있음. 유니코드 문자열→ UTF-8로 인코딩하려면 PyUnicode_AsUTF8String() 을 사용

pymain_run_command() 함수 코드

static int
pymain_run_command(wchar_t *command, PyCompilerFlags *cf)
{
    PyObject *unicode, *bytes;
    int ret;

    unicode = PyUnicode_FromWideChar(command, -1);
    if (unicode == NULL) {
        goto error;
    }

    if (PySys_Audit("cpython.run_command", "O", unicode) < 0) {
        return pymain_exit_err_print();
    }

    bytes = PyUnicode_AsUTF8String(unicode);
    Py_DECREF(unicode);
    if (bytes == NULL) {
        goto error;
    }

    ret = PyRun_SimpleStringFlags(PyBytes_AsString(bytes), cf);
    Py_DECREF(bytes);
    return (ret != 0);

error:
    PySys_WriteStderr("Unable to decode the command from the command line:\n");
    return pymain_exit_err_print();
}

 

pymain_run_command()는 파이썬 바이트열 객체를 PyRun_SimpleStringFlags()로 넘겨서 실행

static int
pymain_run_command(wchar_t *command, PyCompilerFlags *cf)
{
	PyObject *unicode, *bytes;
  int ret;

	unicode = PyUnicode_FromWideChar(command, -1);
	bytes = PyUnicode_AsUTF8String(unicode);
	ret = PyRun_SimpleStringFlags(PyBytes_AsString(bytes), cf);
}

Python/pythonrun.c의 PyRun_SimpleStringFlags()는 문자열을 파이썬 모듈로 변환하고 실행

int
PyRun_SimpleStringFlags(const char *command, PyCompilerFlags *flags)
{
    PyObject *m, *d, *v;
    m = PyImport_AddModule("__main__");   // (1)
    if (m == NULL)
        return -1;
    d = PyModule_GetDict(m);
    v = PyRun_StringFlags(command, Py_file_input, d, d, flags);  // (2)
    if (v == NULL) {
        PyErr_Print();
        return -1;
    }
    Py_DECREF(v);
    return 0;
}

(1) 파이썬 모듈을 독립된 모듈로 실행하려면 __main__ 진입점이 필요하기 때문에 PyRun_simpleStringFlags()가 진입점을 자동으로 추가

(2) PyRun_simpleStringFlags() 는 딕셔너리와 모듈을 만든 후 PyRun_StringFlags() 를 호출해, 가짜 파일 이름을 만들고 파이썬 파서를 실행해 문자열에서 추상 구문 트리(abstract syntax tree, AST)를 생성해 모듈로 반환

5.3.4 로컬 모듈 입력

  • 파이썬의 -m 옵션과 모듈 이름으로 파이썬 명령을 실행할 수도 있음

예시

$ ./python.exe -m unittest
  • 위 명령으로 표준 라이브러리의 unittest 모듈을 실행할 수 있음
  • -m 옵션은 모듈 패키지의 진입점(__main__)을 실행함. 이 때 해당 모듈은 sys.path에서 검색
  • 임포트 라이브러리(importlib)의 검색 메커니즘 덕분에 unittest 모듈의 파일 시스템 위치를 기억할 필요 없음
  • CPython은 표준 라이브러리 모듈 runpy를 임포트하고 PyObject_Call()로 해당 모듈을 실행. runpy 모듈은 Lib/runpy.py에 위치한 순수한 파이썬 모듈임.
  • 임포트는 Python/import.c의 C API 함수 PyImport_ImportModule()이 담당
  • python -m <module> 을 실행하는 것은 python -m runpy <module> 을 실행하는 것과 같음. runpy 모듈은 운영체제에서 모듈을 찾아 실행하는 프로세스를 추상화

runpy는 세단계의 모듈을 실행 …?

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

5.3.5 표준 입력 또는 스크립트 파일 입력

  • python test.py처럼 python을 실행할 때 첫 번째 인자가 파일명이라면 Cpython은 파일 핸들을 열어 Python/pythonrun.c의 PyRun_SimpleFileExFlags()로 핸들을 넘김

이 함수는 세 종류의 파일 경로를 처리할 수 있음

  1. .pyc 파일 경로면 run_pyc_file()을 호출
  2. 스크립트 파일(.py) 경로면 PyRun_FileExFlags()를 호출
  3. <command> | python 처럼 파일 경로가 stdin이면 stdin을 파일 핸들로 취급하고 PyRun_FileExFlags()를 호출
  • stdin이나 스크립트 파일의 경우 CPython은 파일 핸들을 Python/pythonrun.c의 PyRunFileExFlags()로 넘기는데 이는 PyRun_SimpleStringFlags()와 비슷. CPython은 파일 핸들을 PyParser_ASTFromFileObject()로 전달
  • PyRun_SimpleStringFlags() 처럼 PyRunFileExFlags()는 파일에서 파이썬 모듈을 생성하고 run_mode()로 보내 실행

5.3.6 컴파일된 바이트 코드 입력

  • python을 .pyc 파일 경로와 함께 실행하면 CPython은 파일을 텍스트 파일로 불러와 파싱하는 대신 .pyc 파일에서 디스크에 기록된 코드 객체를 찾음
  • PyRun_SimpleFileExFlags()에는 .pyc파일 경로를 처리하는 부분이 있음
  • Python/pythonrun.c의 run_pyc_file()은 파일 핸들을 사용해 .pyc 파일에서 코드 객체를 마셜링함
    • 마셜링은 파일 내용을 메모리를 복사하여 특정 데이터 구조로 변환하는 것을 의미
  • CPython 컴파일러는 스크립트가 호출될 때 마다 파싱하는 대신 디스크의 코드 객체 구조체에 컴파일한 코드를 캐싱
  • 메모리에 마셜링된 코드 객체는 Python/ceval.c 를 호출하는 run_eval_code_obj()로 전달되어 실행됨
반응형

+ Recent posts