이 글은 PyCon Korea 2025에서 진행된 <defer: print(title)> 세션을 정리한 내용입니다. Go의 defer라는 예약어를 Python에 구현하는 것을 주제로, 그 구현 과정을 따라가며 CPython의 내부 구조를 살펴봅니다. 발표 자료는 PyCon Korea 2025 공식 홈페이지에서 확인할 수 있으며, 파이콘 한국 유튜브 채널을 통해 영상으로도 만나보실 수 있습니다. (모든 이미지의 출처는 발표자에게 있습니다.)
defer: print(title)
변도진, 오영기 개발자
PyCon2025에서 Go의 defer라는 예약어를 Python에 구현해 보는 주제로 발표했습니다. 이번 글에선 Cpython의 구조에 대해 더 자세히 살펴보고자 합니다.
이번에 구현해 볼 예약어는 `hello`입니다.

뒤 평가문을 기준으로 Hello, World! 를 출력하는 간단한 예약어 입니다. Python 3.13을 기준으로 합니다. 여기를 참고하세요.
Cpython은 C로 구현한 Python의 공식 Interpreter입니다.
다른 Interpreter들

Cpython의 실행은 크게 두 부분으로 나눌 수 있습니다.
Compile은 Python 코드를 Interpreter(PVM)이 실행 가능한 언어로 변환하는 과정이며, 이는 세부적으로 Parsing과 Compile로 나뉩니다. Runtime은 Interpreter가 코드를 실행하는 과정입니다.
이 글에서는 Compile 과정을 중심으로 알아보겠습니다.
그 전에 Runtime 과정을 살펴보겠습니다.

Runtime의 구조는 위 그림처럼 축약할 수 있습니다.
Runtime의 주요 요소는 다음과 같습니다.
여기서 Compile 과정에 대해 알아야 할 Code, Code와 연관된 Frame만 살펴보겠습니다.
code는 실행 가능한 python 객체입니다. compile된 코드를 정보를 담고 있으며, 이는 독립적으로 실행 가능한 코드 단위로 생성됩니다.

예시

PyFrameObject는 Python level에서 접근 가능한 객체입니다 _PyInterpreterFrame은 PVM에서 사용되는 경량 구조체입니다. 기본적으로 _PyInterpreterFrame을 사용하며, 필요할 때 PyFrameObject를 생성하여 사용합니다.
다음은 python에서의 frame 접근 예제입니다.

이는 다음과 같이 풀이할 수 있습니다.

Cpython의 frame도 살펴봅시다.

frame에는 localsplus라는 요소가 있습니다. 이는 실행 시 로컬 scope + stack입니다.
frame은 eval_loop(_PyEval_EvalFrameDefault)에 의해 실행됩니다.
파이썬(python)에서 토큰(token)은 의미를 가진 최소 단위입니다. 파이썬의 토큰은 grammar/tokens에 저장되어 있고, token은 줄넘김을 의미하는 NEWLINE, 들여쓰기를 의미하는 INDENT, 각종 이름들(변수명, 함수명, 예약어 등)을 의미하는 NAME, 각종 연산자와 특수기호 등이 있습니다. (더 많은 토큰의 종류에 대해서는 문서를 참고하세요.)
예약어란 컴퓨터 프로그래밍 언어에서 이미 문법적인 용도로 사용되고 있기 때문에, 식별자로 사용할 수 없는 단어를 말합니다. 예를 들면, if, while, for 등이 있습니다.
파이썬의 기본적인 문법은 Grammar/python.gram 파일에 저장되어 있습니다. python.gram에는 파이썬의 예약어들과 기본적인 문장 구조들의 규칙이 정의되어 있습니다. python.gram은 파서 표현식 문법(parsing expression grammar, 약칭 PEG)를 사용합니다. PEG에 대한 자세한 내용은 이 글을 참고하세요.
이것은 python.gram에 적혀있는 파이썬의 while문에 관한 문법이고, 문법을 파이썬 스타일과 매칭시켜 보았을 때 다음과 같이 표현할 수 있습니다.

while문에는 다음과 같은 PEG의 표기법들이 사용되었습니다.
*PEG의 더 많은 표기법은 링크를 참고하세요.
named_expression 규칙에는 대입 표현식과 일반 표현식이 들어가 있습니다. 표현식(expression)은 하나 이상의 값으로 표현될 수 있는 코드를 말합니다.

block에는 다음과 같이
1. 줄 바꿈 들여쓰기 후 문장들(statements) 또는
2. 단순문(simple_stmts)이 들어갈 수 있습니다.


statement는 실행할 수 있는 최소 코드 단위이고, simple_stmts는 1줄로 나타낼 수 있는 모든 표현들을 뜻합니다.
표현식이 참이면 Hello, world!를 출력하는 hello라는 예약어를 만든다고 하면, 이런 식으로 문법을 구성할 수 있습니다.

AST란 소스 코드가 가지고 있는 구조를 트리 형태로 나타낸 것으로 구문 분석, 즉 뒤에 나올 토크나이저와 파서가 추구하는 최종 결과물입니다.

이것은 위에 있는 함수의 ast를 instaviz 라이브러리를 이용하여 시각화 한 자료입니다.

AST를 만들기 위해서는 ASDL(abstract syntax description language) 파일에 ast node의 속성을 정의해 주어야 합니다. 아래에 있는 이미지는 python.asdl 파일에 정의되어 있는 while문입니다.


토크나이저(tokenizer)는 문장들을 토큰으로 변환시키는 역할을 합니다.

아래 출력 결과는 위의 소스 코드가 토큰으로 변환된 결과물을 tokenize 모듈을 통해서 출력한 것입니다.

파서(parser)는 토큰을 AST로 변환시키는 역할을 합니다.
위에서 만들었던 예약어의 AST 노드를 정의한다고 하면,

다음과 같이 ASDL 파일에 정의하여 AST 노드를 만들 수 있습니다.

그 후에 ast node에 올바른 정보가 들어있는지 검증하는 코드를 추가합니다.
심볼 테이블(symtable)은 파이썬의 전역과 지역 식별자들을 저장하는 데이터 구조입니다. symtable은 ast 노드를 탐색하고 저장한 뒤, 컴파일러에 데이터를 제공하여 컴파일러가 알맞게 스코프를 결정하여 참조할 수 있게 해 줍니다.
ast node를 새로 만들었기 때문에 symtable이 읽을 수 있도록 Hello 처리 코드를 추가합니다.

컴파일은 ast를 bytecode로 변환하는 과정을 의미합니다. 이 과정을 맞는 것이 Compiler입니다.
Compiler는 컴파일 과정을 맡는 유닛입니다.
구조는 다음과 같습니다.

다음과 같이 동작합니다.
while의 컴파일 과정을 통해 구문이 어떻게 컴파일되는지 살펴봅시다.

이를 보기 쉽게 Python 스타일로 표현하면 다음과 같습니다.

bytecode는 PVM(Interpreter)가 실행하는 명령어입니다. 이는 다음과 같은 구조를 가집니다.

dis 모듈을 사용하여 확인할 수 있습니다.

정리하면 다음과 같습니다.

그럼 흐름을 천천히 따라가 보겠습니다.
1) RESUME
frame에 진입하고 설정합니다.
2) LOAD_FAST_LOAD_FAST (a, b)
stack에 a, b를 push합니다.

3) BINARY_OP (+)
stack에 a, b를 pop 합니다. a + b의 결과(AB)를 stack의 push합니다.

4) STORE_FAST (res)
stack에 AB를 pop합니다. res에 저장합니다.

5) LOAD_FAST (res)
stack에 res를 push합니다.

6) RETURN_VALUE
stack에 res를 pop합니다. res를 반환합니다.

이번엔 BINARY_OP를 예시로 볼게요.

이는 BINARY_OP를 정의한 것입니다. _SPECIALIZE_BINARY_OP는 최적화 경로의 엔트리이며, _BINARY_OP는 정석적인 경로입니다. 이는 uop(micro-instruction)의 조합으로 하나의 instruction를 구성한 것입니다.
그럼 _BINARY_OP와 _SPECIALIZE_BINARY_OP를 살펴볼게요.

– 를 기준으로 input과 output을 구별합니다. BINARY_OP는 oparg에 맞는 연산을 호출하여 결과를 계산합니다. (오버헤드 큼 -> 계산 객체의 method call)


inline cache의 counter를 가져와, 실행 횟수(cache hit)를 확인 후 조건을 충족하면 specializtion instruction으로 전환합니다.


int + int일 때의 specialization instruction

(전체 flow)

input이 int인지 확인합니다.

이런 식으로 최적화가 이루어집니다.
LOAD_FAST_LOAD_FAST 같은 super-instruction도 있습니다.

dis의 adaptive와 show_caches를 켜서 specialization instruction이 작동하는 것과 cache의 counter가 변하는 것을 확인할 수 있습니다.
이번엔 hello의 bytecode, SAY_HELLO를 만들어 보겠습니다. SAY_HELLO는 평가 시 Hello, World!\n을 출력하는 bytecode입니다.

이후 make regen-all을 하면 bytecode가 생성됩니다.
hello의 compiler를 만들어 보겠습니다.

hello의 compiler를 구현했습니다. make -j3를 입력하여 빌드 후 실행하면, 다음과 같이 동작합니다.


이로써 예약어 Hello를 구현해 봤습니다.
Python은 지금까지 살펴본 것처럼 동작합니다. 저희는 예약어를 직접 만들어 보며 그 과정 속에서 Python의 내부 동작을 함께 살펴볼 수 있었습니다. 사실 실제로 예약어를 만들 일은 거의 없으며, Python의 내부 동작을 깊이 고려해야 할 상황도 많지 않습니다. 그럼에도 불구하고, Python의 내부 동작을 이해해 보는 것은 Python 사용자로서 충분히 의미 있는 일이라고 생각합니다. 이 글이 여러분께 도움이 되기를 바랍니다.