요즘IT
위시켓
AIDP
콘텐츠프로덕트 밸리
요즘 작가들컬렉션물어봐
놀이터
콘텐츠
프로덕트 밸리
요즘 작가들
컬렉션
물어봐
놀이터
새로 나온
인기
개발
AI
IT서비스
기획
디자인
비즈니스
프로덕트
커리어
트렌드
스타트업
서비스 전체보기
위시켓요즘ITAIDP
고객 문의
02-6925-4867
10:00-18:00주말·공휴일 제외
yozm_help@wishket.com
요즘IT
요즘IT 소개작가 지원
기타 문의
콘텐츠 제안하기광고 상품 보기
요즘IT 슬랙봇크롬 확장 프로그램
이용약관
개인정보 처리방침
청소년보호정책
㈜위시켓
대표이사 : 박우범
서울특별시 강남구 테헤란로 211 3층 ㈜위시켓
사업자등록번호 : 209-81-57303
통신판매업신고 : 제2018-서울강남-02337 호
직업정보제공사업 신고번호 : J1200020180019
제호 : 요즘IT
발행인 : 박우범
편집인 : 노희선
청소년보호책임자 : 박우범
인터넷신문등록번호 : 서울,아54129
등록일 : 2022년 01월 23일
발행일 : 2021년 01월 10일
© 2013 Wishket Corp.
로그인
요즘IT 소개
콘텐츠 제안하기
광고 상품 보기
개발

예약어가 되기까지: CPython 딥다이브

파이썬 한국 사용자 모임
8분
1시간 전
84
에디터가 직접 고른 실무 인사이트 매주 목요일에 만나요.
newsletter_profile0명 뉴스레터 구독 중

이 글은 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
  • Parsing
  • Compile

 

Cpython

Cpython은 C로 구현한 Python의 공식 Interpreter입니다.

 

다른 Interpreter들

  • PyPy(Python)
  • Jython(Java)
  • RustPython(Rust)

 

구조 살펴보기

 

Cpython의 실행은 크게 두 부분으로 나눌 수 있습니다.

  1. Compile
  2. Runtime

 

Compile은 Python 코드를 Interpreter(PVM)이 실행 가능한 언어로 변환하는 과정이며, 이는 세부적으로 Parsing과 Compile로 나뉩니다. Runtime은 Interpreter가 코드를 실행하는 과정입니다.

 

이 글에서는 Compile 과정을 중심으로 알아보겠습니다.

 

Runtime

그 전에 Runtime 과정을 살펴보겠습니다.

 

구조 살펴보기

 

Runtime의 구조는 위 그림처럼 축약할 수 있습니다.

 

주요 요소

Runtime의 주요 요소는 다음과 같습니다.

 

  • pyruntimestate: Include/internal/pycore_runtime.h
    Runtime 본체
  • PyInterpreterState(_is) : Include/cpython/pycore_interp.h
    코드 실행기
  • PyThreadState(_ts) : Include/internal/pystate.h
    실행할 코드 작업
  • PyCodeObject : Include/cpython/code.h
    compile된 코드 객체
  • PyFrameObject(_frame) : Include/internal/pycore_frame.h
    PyCodeObject의 실행 인스턴스
  • _PyInterpreterFrame : Include/internal/pycore_frame.h
    frame의 실행용 가벼운 인스턴스

 

여기서 Compile 과정에 대해 알아야 할 Code, Code와 연관된 Frame만 살펴보겠습니다.

 

Code(PyCodeObject)

code는 실행 가능한 python 객체입니다. compile된 코드를 정보를 담고 있으며, 이는 독립적으로 실행 가능한 코드 단위로 생성됩니다.

 

 

예시

 

Frame(PyFrameObject, _PyInterpreterFrame)

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

 

다음은 python에서의 frame 접근 예제입니다.

 

 

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

 

 

Cpython의 frame도 살펴봅시다.

 

frame에는 localsplus라는 요소가 있습니다. 이는 실행 시 로컬 scope + stack입니다.

frame은 eval_loop(_PyEval_EvalFrameDefault)에 의해 실행됩니다.

 

  • _PyEval_EvalFrameDefault : Python.ceval.c
 

Python 문법 살펴보기

Tokens

파이썬(python)에서 토큰(token)은 의미를 가진 최소 단위입니다. 파이썬의 토큰은 grammar/tokens에 저장되어 있고, token은 줄넘김을 의미하는 NEWLINE, 들여쓰기를 의미하는 INDENT, 각종 이름들(변수명, 함수명, 예약어 등)을 의미하는 NAME, 각종 연산자와 특수기호 등이 있습니다. (더 많은 토큰의 종류에 대해서는 문서를 참고하세요.)

 

예약어

예약어란 컴퓨터 프로그래밍 언어에서 이미 문법적인 용도로 사용되고 있기 때문에, 식별자로 사용할 수 없는 단어를 말합니다. 예를 들면, if, while, for 등이 있습니다.

 

python.gram

파이썬의 기본적인 문법은 Grammar/python.gram 파일에 저장되어 있습니다. python.gram에는 파이썬의 예약어들과 기본적인 문장 구조들의 규칙이 정의되어 있습니다. python.gram은 파서 표현식 문법(parsing expression grammar, 약칭 PEG)를 사용합니다. PEG에 대한 자세한 내용은 이 글을 참고하세요.

 

이것은 python.gram에 적혀있는 파이썬의 while문에 관한 문법이고, 문법을 파이썬 스타일과 매칭시켜 보았을 때 다음과 같이 표현할 수 있습니다.

 

 

while문에는 다음과 같은 PEG의 표기법들이 사용되었습니다.

  • 규칙 옆의 [] 안에는 리턴 타입을 표시합니다.
  • invalid로 시작하는 규칙 이름은 구문 오류에 사용됩니다.
  • |은 대안을 표현합니다.
  • []은 선택적인 부분을 표현합니다.
  • {}로 값을 반환하는 부분을 표현합니다.
  • &&는 다음 토큰을 파싱하였을 때 존재하지 않으면 즉시 Error를 발생시킵니다.

 

*PEG의 더 많은 표기법은 링크를 참고하세요.

 

named_expression 규칙에는 대입 표현식과 일반 표현식이 들어가 있습니다. 표현식(expression)은 하나 이상의 값으로 표현될 수 있는 코드를 말합니다.


 



 

block에는 다음과 같이

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


 

 

statement는 실행할 수 있는 최소 코드 단위이고, simple_stmts는 1줄로 나타낼 수 있는 모든 표현들을 뜻합니다.

 

 

새로운 예약어 정의

표현식이 참이면 Hello, world!를 출력하는 hello라는 예약어를 만든다고 하면, 이런 식으로 문법을 구성할 수 있습니다.

 

 

parsing

AST(abstract syntax tree)

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

 

 

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

 

 

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

 



 

tokenizer

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

 

 

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

 

parser

파서(parser)는 토큰을 AST로 변환시키는 역할을 합니다.

 

AST 노드 새로 정의 및 검증 추가

위에서 만들었던 예약어의 AST 노드를 정의한다고 하면,


 

 

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

 

 

그 후에 ast node에 올바른 정보가 들어있는지 검증하는 코드를 추가합니다.

 

symtable

심볼 테이블(symtable)은 파이썬의 전역과 지역 식별자들을 저장하는 데이터 구조입니다. symtable은 ast 노드를 탐색하고 저장한 뒤, 컴파일러에 데이터를 제공하여 컴파일러가 알맞게 스코프를 결정하여 참조할 수 있게 해 줍니다.

 

symtable에 Hello 저장

ast node를 새로 만들었기 때문에 symtable이 읽을 수 있도록 Hello 처리 코드를 추가합니다.

 

 

Compile

컴파일은 ast를 bytecode로 변환하는 과정을 의미합니다. 이 과정을 맞는 것이 Compiler입니다.

 

Compiler

Compiler는 컴파일 과정을 맡는 유닛입니다.

 

  • compiler : Python/compiler.c
    전체 컴파일을 관장하는 유닛입니다.
  • compiler_unit : Python/compiler.c
    스코프 단위의 Block을 컴파일하는 유닛입니다.

 

구조는 다음과 같습니다.

 

 

다음과 같이 동작합니다.

  1. Compiler가 컴파일할 Block을 찾으면 CompilerUnit을 생성해 할당합니다.
  2. CompilerUnit이 Block을 컴파일하여 Bytecode로 변환합니다.
  3. 변환이 끝나면 Compiler는 CompilerUnit의 Bytecode를 assemble하여 CodeObject로 반환합니다.

 

구문의 컴파일

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

 

 

  • Label: Jump 가능 지점 표시자
  • Jump: 실행 위치 이동(goto)
  • fblock: 제어 흐름이 중첩된 블록 구조를 추적하기 위한 스택 구조
                (continue, `break` 같은 예약어를 위함)
  • visit: AST의 Node를 방문하여 컴파일함

 

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

 

 

Bytecode

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

 

 

  • oparg: bytecode의 매개변수.
  • opcode: bytecode의 명령어 번호
  • oparg: bytecode의 매개변수.
  • opcode: bytecode의 명령어 번호

 

Bytecode 예제

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를 반환합니다.
 

 

Bytecode 정의

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

 

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

 

  • instruction : bytecode를 이루는 하나의 명령
  • micro-instruction : instruction을 이루는 세부 명령

 

그럼 _BINARY_OP와 _SPECIALIZE_BINARY_OP를 살펴볼게요.

 

 

  • _BINARY_OP : instruction 이름
  • lhs, rhs : stack에서 pop해오는 input
  • res : stack에 push하는 output
  • op : uop 선언 문법
  • inst : instruction 선언 문법

 

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


 

 

  • specializing : specializtion instruction 시작 지점
  • <name>/<num> : inline cache 사용
                                <num>만큼의 cache 사용

 

 

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

 

 

  • family: BINARY_OP의 코드를 가지는 그룹 정의

 

 

int + int일 때의 specialization instruction

 

 

(전체 flow)

 

input이 int인지 확인합니다.

  • EXIT_IF : 인자가 참일 시 해당 분기 종료
  • DEOPT_IF : 인자가 참일 시 specialization instruction을 기본 instruction으로 되돌림

 

 

이런 식으로 최적화가 이루어집니다.

  • inline cache 부연 설명: bytecode에 각 instruction에 할당되어 있음, instruction과 cache를 합쳐 slot이라 합니다.

 

LOAD_FAST_LOAD_FAST 같은 super-instruction도 있습니다.

  • super-instruction : instruction 여러 개 합친 기능(최적화)
                                LOAD_FAST_LOAD_FAST -> LOAD_FAST 2개

 

예시

 

dis의 adaptive와 show_caches를 켜서 specialization instruction이 작동하는 것과 cache의 counter가 변하는 것을 확인할 수 있습니다.

 

 

Hello 구현하기

Bytecode

이번엔 hello의 bytecode, SAY_HELLO를 만들어 보겠습니다. SAY_HELLO는 평가 시 Hello, World!\n을 출력하는 bytecode입니다.

 

 

이후 make regen-all을 하면 bytecode가 생성됩니다.

 

compiler

hello의 compiler를 만들어 보겠습니다.

 

 

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

 

 

이로써 예약어 Hello를 구현해 봤습니다.

 

 

마치며

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