본문 바로가기

COMPUTER SCIENCE/PYTHON

[CPython 파헤치기] 사실 Python은 C로 구성되어 있다 - CPython 구성 요소 살펴보기

해당 글은 도서 CPython 파헤치기 1-4장을 기반으로 작성되었습니다.

 

Python은 아래 2023년 stack overflow survey의 결과에서도 보여주고 있듯이 많은 개발자들에게 사랑받고 있는 언어 중 하나이다.

실제로 업무에서나 다른 개발 작업을 할 때 내가 가장 주 언어로 사용하고 있는 것이 Python인데,
정작 이렇게 많이 사용하고 있으면서 내부 구조를 어떻게 구성되어 있는지 알지 못해 매번 궁금했었다.

그래서 이번 글에서는 CPython 파헤치기 관련 첫번째 글로,
C로 구성되어 있는 Python의 전반적인 내부 구조와 문법에 대해 살펴보고자 한다.

 

C로 구성되어 있는 Python

우리가 일반적으로 이용하고 있는, 공식 사이트 python.org에서 다운로드 받는 python은 C로 작성되어 있다.

새로운 프로그래밍 언어를 만들려면 한 언어(출발어: source language)를 다른 만들고자 하는 언어(도착어: target language)로 바꿔줄 번역기와 같은 컴파일러가 필요하다.
새로운 언어 개발 시 어떤 프로그램이든 실행할 수 있어야 하기 때문에 보통 더 오래되고 안정적인 언어로 컴파일러를 작성한다.

컴파일러의 유형은 어떤 언어로 작성했는지에 따라 다음과 같이 구분할 수 있다.

  • 셀프 호스팅 컴파일러
    - 자기 자신으로 작성한 컴파일러로, 부트스트래핑 단계를 통해 만들어진다.

    - 예를 들어, Go 언어는 원래 C로 작성되었는데 C로 작성된 첫번째 Go 컴파일러가 Go를 컴파일할 수 있게 되자
    컴파일러를 Go로 재작성했다. (이와 같은 과정을 부트스트래핑 단계로 볼 수 있다.)
  • 소스 대 소스(source-to-source) 컴파일러
    - 이미 배포되어 있는 다른 언어로 작성한 컴파일러를 말한다.

    - 여기에서 살펴볼 CPython이 해당되며, C를 기반으로 Python이 작성되었다.

여러 언어 중 C로 구성된 이유는, 간단히 요약하자면 안정적인 언어다양한 표준 라이브러리 모듈을 이용하기 위해서이다.

여러 표준 라이브러리 모듈(ssl, sockets 등)도 저수준 운영체제 API에 접근하기 위해서 C로 작성되어 있고,
네트워크 소켓 만들기, 파일 시스템 조작, 디스플레이와 상호작용하는 윈도우/리눅스 커널 API도 모두 C로 작성되어 있다.
따라서 Python 또한 확장성을 고려하여 C로 작성되었다고 볼 수 있다.

공식으로는 C로 구성되어 있는 Python을 제공하지만, 다음과 같이 다른 언어로 구성되어 있는 여러 Python 구현체가 존재한다.

  • Jython
    - Java로 작성된 파이썬 구현체

    - JVM(Java Virtual Machine)에서 실행
  • IronPython
    - C#로 작성된 파이썬 구현체

    - .NET 프레임워크 사용
  • PyPy
    - Python으로 작성된 셀프 호스팅 컴파일러 기반 파이썬 구현체

    - Python 정적 타입으로 작성
      (정적 타입: 실행하기 전에 변수의 type을 미리 결정하고, 그 이후에는 type을 변경하지 않는 방식)
    - JIT 컴파일러 방식으로 구현되어 기존 interpreter 방식보다 빠르고 효율적이다.
      (JIT(Just-In-Time) 컴파일러: 프로그램을 실행하는 동안 실시간으로 기계어로 변환하는 컴파일러)

 

CPython 배포판 구성 요소

공식 CPython 배포판 https://github.com/python/cpython 에서 소스 코드를 살펴보면 아래와 같은 구조로 구성되어 있다.

cpython
├── CODE_OF_CONDUCT.md
├── Doc         # 문서 소스 파일
├── Grammar     # 컴퓨터가 읽을 수 있는 언어 정의
├── Include     # C 헤더 파일
├── LICENSE
├── Lib         # 파이썬으로 작성된 표준 라이브러리 모듈
├── Mac         # macOS를 위한 파일
├── Makefile.pre.in
├── Misc        # 기타 파일
├── Modules     # C로 작성된 표준 라이브러리 모듈
├── Objects     # 코어 타입과 객체 모델
├── PC          # 이전 버전의 윈도우를 위한 윈도우 빌드 지원 파일
├── PCbuild     # 윈도우 빌드 지원 파일
├── Parser      # 파이썬 파서 소스 코드
├── Programs    # python 실행 파일과 기타 바이너리를 위한 소스 코드
├── Python      # CPython 인터프리터 소스 코드
├── README.rst
├── Tools       # CPython 빌드하거나 확장하는 데 유용한 독립 실행형 도구
├── aclocal.m4
├── config.guess
├── config.sub
├── configure
├── configure.ac
├── install-sh
├── pyconfig.h.in
└── setup.py

 

구성 요소를 크게 5가지로 나눠보자면 다음과 같다.

1. 언어 사양 (Language Specification)

우리가 영어를 배울 때 문법을 함께 배웠듯이,
컴파일러가 언어를 실행하기 위해서는 해당 언어의 문법, 구문, 의미론에 대한 규칙인 언어 사양이 필요하다.

예를 들어, []는 빈 리스트를 생성할 뿐만 아니라 인덱싱, 슬라이싱을 위해 사용된다는 점이다.

# 빈 리스트 생성
list_empty = []

list_example = [1,2,3,4,5]
list_example[0]    # 인덱싱
list_example[2:4]  # 슬라이싱

 

언어 사양은 문법 형식과 각 문법 요소가 실행되는 방식을 자세히 설명하고 있으며,
사람이 읽을 수 있는 형식과 기계가 읽을 수 있는 형식으로 제공하고 있다.

먼저 사람이 읽을 수 있는 형식은 Doc/reference 경로에 .rst(reStructuredText) 형식으로 저장되어 있다.

Doc/reference
├── index.rst               # 언어 레퍼런스 목차
├── introduction.rst        # 레퍼런스 문서 개요
├── compound_stmts.rst      # 복합문 (if, while, for, 함수 정의 등)
├── datamodel.rst           # 객체, 값, 타입
├── executionmodel.rst      # 프로그램 구조
├── expressions.rst         # 표현식 구성 요소
├── grammar.rst             # 문법 규격(Grammar/Grammar 참조)
├── import.rst              # import 시스템
├── lexical_analysis.rst    # 어휘 구조 (줄, 들여쓰기, 토큰, 키워드 등)
├── simple_stmts.rst        # 단순문 (assert, import, return, yield 등)
└── toplevel_components.rst # 스크립트 및 모듈 실행 방법 설명

 

마치 백과사전을 읽는 것처럼 각 레퍼런스에 대해 정의되어 있다.

아래는 흔히 사용해오던 if문과 관련된 설명으로,
elif 및 else 키워드와 함께 사용될 수 있으며 true인 expression이 나올 때까지 평가해 나간다고 정의되어 있다.

.. _if:
.. _elif:
.. _else:

The :keyword:`!if` statement
============================

.. index::
   ! statement: if
   keyword: elif
   keyword: else
   single: : (colon); compound statement

The :keyword:`if` statement is used for conditional execution:

.. productionlist:: python-grammar
   if_stmt: "if" `assignment_expression` ":" `suite`
          : ("elif" `assignment_expression` ":" `suite`)*
          : ["else" ":" `suite`]

It selects exactly one of the suites by evaluating the expressions one by one
until one is found to be true (see section :ref:`booleans` for the definition of
true and false); then that suite is executed (and no other part of the
:keyword:`if` statement is executed or evaluated).  If all expressions are
false, the suite of the :keyword:`else` clause, if present, is executed.

 

다음으로 기계가 읽을 수 있는 형식은 Grammar 경로에 저장되어 있다.

이 중 Grammar/python.gram 파일에서는
각 문법을 파서 표현식 문법 PEG(Parsing Expression Grammar) 사양을 통해 정의되어 있으며,
마치 정규표현식처럼 *는 반복을, +는 최소 1번 이상 반복을, |는 or 등을 나타낸다.

예를 들어 while 문에 대해서는 아래와 같이 3가지 케이스가 존재한다.

# 1) 기본 표현식
while finished:
    do_things()
    
# 2) named_expression 대입 표현식 사용
## (값을 할당하는 동시에 그 값을 평가하는 표현식)
while letters := read(document, 10):
    print(letters)

# 3) else 블록 사용
while item := next(iterable):
    print(item)
else:
    print("Iterable is empty")

 

이에 대해서 python.gram 파일에는 다음과 같이 정의되어 있다.

# Grammar/python.gram#L165

while_stmt[stmt_ty]:
    | 'while' a=named_expression ':' b=block c=[else_block] ...

 

2. 컴파일러 (Interpreter)

C 언어로 작성된 컴파일러로, 파이썬 소스 코드 → 실행 가능한 기계어로 변환하는 역할

컴파일러는 Python 경로 내의 C 언어 파일들로 구성되어 있으며,
코드 실행 및 내부 메모리 관리, 내장 함수의 구현 등이 포함되어 있다.

배포되어 있는 C 기반 소스 코드를 Python으로 컴파일 하는 과정은 다음과 같다.
아래와 같이 컴파일하게 되면 python.exe 파일이 생성되며, 해당 파일을 통해 소스 코드를 기반으로 생성된 python을 이용할 수 있다.

(Mac 기준)

# C 컴파일러 툴킷(Xcode Command Line Tools) 설치
xcode-select --install  ## make, GNU Compiler Collection(gcc) 등 설치

# 외부 라이브러리 설치
brew install openssl xz zlib gdbm sqlite

# Makefile 생성 (configure 스크립트 실행)
CPPFLAGS="-I$(brew --prefix zlib)/include" \
LDFLAGS="-L$(brew --prefix zlib)/lib" \
./configure --with-openssl=$(brew --prefix openssl) --with-pydebug

## 애플칩(M1, M2, ...)은 아래와 같이 xz 경로도 추가
CPPFLAGS="-I$(brew --prefix zlib)/include -I$(brew --prefix xz)/include" \
LDFLAGS="-L$(brew --prefix zlib)/lib -L$(brew --prefix xz)/lib" \
./configure --with-openssl=$(brew --prefix openssl) --with-pydebug

# CPython 바이러니 빌드
make -j2 -s   # -j2: 동시 작업 2개 / -s: 실행된 명령어 출력 X

# 바이너리 파일 실행
./python.exe

 

(Windows 기준)

# 의존성 설치 (외부 도구, 라이브러리, C 헤더 등 설치)
PCbuild/get_externals.bat

# PCbuild/amd64/python_d.exe 바이너리 파일 생성
build.bat -p x64 -c Debug    # 디버그 버전
# 디버그 버전 바이너리 파일 실행
amd64\python_d.exe

# PCbuild/amd64/python.exe 바이너리 파일 생성
build.bat -p x64 -c Release  # 릴리즈 버전 (프로파일 기반 최적화 구성 사용됨)
# 릴리즈 버전 바이너리 파일 실행
amd64\python.exe

 

3. 표준 라이브러리 모듈 (Standard Library Modules)

파일 입출력, 네트워킹, 문자열 처리, 데이터 구조, GUI 프로그래밍 등 Python과 함께 기본적으로 제공되는 패키지들이 존재한다.
Lib 경로에는 Python으로 작성된 코드가, Modules 경로에는 C로 작성된 확장 모듈이 존재한다.

Lib 경로에는 흔히 자주 사용해오던 os, sys, re, json 같은 라이브러리가 정의되어 있다.
대표적으로 re에서의 match 함수를 보면, 아래와 같이 _compile 한 뒤 match가 실행되는 것을 볼 수 있다.
사용할 정규표현식을 compile하지 않고 match를 이용하게 되면 매번 compile을 실행해주기 때문에,
자주 사용하는 정규표현식의 경우 먼저 compile을 한 뒤 이용하면 더욱 빠르게 이용할 수 있다는 점을 해당 코드를 통해 알 수 있다.

def match(pattern, string, flags=0):
    """Try to apply the pattern at the start of the string, returning
    a Match object, or None if no match was found."""
    return _compile(pattern, flags).match(string)

 

4. 코어 타입 (Core Types)

Python에서의 내장 데이터 타입은 Objects 경로에 구현되어 있다.
여기에는 숫자, 문자열, list, tuple, dictionary 등의 코어 타입과 객체 모델이 C 언어로 구현되어 있다.

예를 들어 list는 Objects/listobject.c 파일에 정의되어 있으며,
Include/cpython/listobject.h 헤더 파일에 아래와 같이 C 구조체가 정의되어 있다.

typedef struct {
    PyObject_VAR_HEAD      // 파이썬 가변 길이 객체에 대한 표준 헤더
    PyObject **ob_item;    // 리스트의 요소를 가리키는 포인터 배열
    Py_ssize_t allocated;  // 배열에 할당된 메모리의 크기
} PyListObject;

list에서 사용하는 method들은 Objects/listobject.c 파일에 PyList_Append, PyList_Extend 등을 살펴보면 된다.

int
PyList_Append(PyObject *op, PyObject *newitem)
{
    if (PyList_Check(op) && (newitem != NULL))
        return app1((PyListObject *)op, newitem);
    PyErr_BadInternalCall();
    return -1;
}

static int
app1(PyListObject *self, PyObject *v)
{
    Py_ssize_t n = PyList_GET_SIZE(self);

    assert (v != NULL);
    if (n == PY_SSIZE_T_MAX) {
        PyErr_SetString(PyExc_OverflowError,
            "cannot add more objects to list");
        return -1;
    }

    if (list_resize(self, n+1) < 0)
        return -1;

    Py_INCREF(v);
    PyList_SET_ITEM(self, n, v);
    return 0;
}

 

5. 테스트 스위트 (Test Suite)

마지막으로 테스트 스위트는 개발 및 유지 보수에 사용되는 테스트 모음으로, 유닛 테스트, 통합 테스트, 성능 테스트이 포함된다.
CPython 개발과 유지보수를 위해 사용되는 다양한 테스트들로 주로 Lib/test 경로의 파이썬 스크립트들로 구성되어 있다.
해당 테스트들을 통해 CPython의 변경 사항이 파이썬 언어 사양을 정확히 구현하고 있는지, 그리고 기존 기능들이 여전히 정상적으로 작동하는지 확인할 수 있다.

예를 들어, Lib/test/test_dict.py의 test_keys 함수는 아래와 같이 dictionary의 key와 관련된 테스트를 진행한다.

def test_keys(self):
    # 비어있는 dict에 대한 key 확인
    d = {}
    self.assertEqual(set(d.keys()), set())

    # element가 있는 dict에 대한 key 확인
    d = {'a': 1, 'b': 2}
    k = d.keys()
    self.assertEqual(set(k), {'a', 'b'})

    # key 존재 여부 테스트
    self.assertIn('a', k)
    self.assertIn('b', k)
    self.assertIn('a', d)
    self.assertIn('b', d)

    # 예외 발생 테스트
    self.assertRaises(TypeError, d.keys, None)

    # 문자열 표현 테스트
    self.assertEqual(repr(dict(a=1).keys()), "dict_keys(['a'])")

 

이처럼 공식으로 제공하는 Python 내부 구조는 C로 구현되어 있으며,
배포되어 있는 소스 코드에는 언어 사양, 컴파일러, 표준 라이브러리 모듈, 내장 데이터 타입 및 테스트 등으로 구성되어 있다.

Python으로 작업을 진행하다 좀 더 깊이 있게 살펴봐야 하는 상황이라면
해당 구성 요소를 참고해서 소스 코드를 들여다보는 것도 하나의 방법이 될 수 있을 것 같다.

다음 글에서는 Python이 입력받는 방식과 렉싱과 파싱 등에 관해 다뤄보고자 한다.

반응형