Python 실행 구조 완벽 정리: 인터프리터, AST, 바이트코드, PVM까지

반응형

데이터 분석가가 알아야 할 Python 실행 구조

Python은 데이터 분석과 AI/ML 분야에서 사실상 표준 언어처럼 쓰인다.
NumPy, Pandas, scikit-learn, PyTorch, TensorFlow는 물론이고 LangChain, RAG 파이프라인, LLM 애플리케이션 개발까지 Python을 중심으로 돌아가는 경우가 많다. Python이 이렇게 널리 쓰이는 이유는 단순히 “문법이 쉬워서”만은 아니다. Python은 데이터를 다루는 개발 방식과 잘 맞는다. 코드를 짧게 작성할 수 있고, REPL이나 Jupyter Notebook에서 바로 실행해보며 결과를 확인할 수 있다. 분석가는 처음부터 완성된 프로그램을 만드는 사람이 아니라, 데이터를 보면서 가설을 세우고, 코드를 바꾸고, 다시 확인하는 일을 반복한다. Python은 이 흐름에 매우 적합하다.

그런데 Python을 설명할 때 자주 나오는 말이 있다.

Python은 인터프리터 언어로, 소스코드를 한 줄씩 읽고 바로 실행한다.

 

이 설명은 완전히 틀린 말은 아니지만, 실제 Python의 실행 구조를 설명하기에는 조금 부족하다. CPython 기준으로 Python은 단순히 .py 파일을 한 줄씩 읽고 끝내는 구조가 아니다. 내부적으로 소스코드를 분석하고, AST를 만들고, 바이트코드로 컴파일한 다음, Python Virtual Machine에서 실행한다. 즉 Python은 “그냥 한 줄씩 실행되는 언어”라기보다, 컴파일과 인터프리트가 함께 있는 실행 구조를 가진 언어에 가깝다.

Python이 데이터 분석에 강한 이유

데이터 분석에서 중요한 것은 빠른 피드백이다. CSV 파일을 읽고, 결측치를 확인하고, 컬럼을 바꾸고, 그래프를 그려보고, 모델을 학습시키는 과정은 대부분 반복적이다. 코드를 작성한 뒤 결과를 바로 확인할 수 있어야 한다.

Python의 REPL 환경은 이 점에서 강력하다.

>>> import pandas as pd
>>> df = pd.read_csv("sales.csv")
>>> df.head()

 

이런 식으로 데이터를 조금씩 확인하면서 분석을 진행할 수 있다. 전체 프로그램을 컴파일하고 실행 파일을 만들 필요 없이, 필요한 코드를 바로 실행하고 결과를 볼 수 있다. 또한 Python 생태계는 데이터 분석에 필요한 거의 모든 도구를 갖추고 있다.

NumPy        : 배열 연산
Pandas       : 데이터프레임 분석
Polars       : 고성능 데이터프레임 처리
DuckDB       : 로컬 SQL 분석
scikit-learn : 머신러닝
PyTorch      : 딥러닝
TensorFlow   : 딥러닝
LangChain    : LLM 애플리케이션 개발

대부분은 pip로 설치할 수 있다.

pip install pandas numpy scikit-learn

이 접근성은 생각보다 중요하다. 데이터 분석가는 언어 자체보다 문제 해결에 집중해야 한다. Python은 필요한 도구를 빠르게 가져오고, 짧은 코드로 실험할 수 있게 해준다.

Python 실행 흐름 한눈에 보기

Python 코드가 실행되는 흐름은 대략 다음과 같다.

소스코드(.py)
→ Parser
→ AST
→ Bytecode
→ Python Virtual Machine
→ 실행 결과

예를 들어 이런 코드가 있다고 해보자.

def add(x, y):
    return x + y

print(add(3, 5))

Python은 이 코드를 바로 CPU가 실행하는 기계어로 바꾸지 않는다. 먼저 문법을 분석하고, 코드 구조를 AST로 만든 뒤, Python VM이 이해할 수 있는 바이트코드로 컴파일한다. 그리고 PVM이 그 바이트코드를 실행한다. 그래서 Python은 인터프리터 언어라고 부르지만, 내부적으로는 컴파일 단계가 존재한다. 정확히 말하면 CPython은 다음과 같은 구조를 가진다.

소스코드 작성
→ 문법 분석
→ AST 생성
→ 바이트코드 컴파일
→ PVM에서 실행

이 구조를 이해하면 __pycache__ 디렉터리가 왜 생기는지, .pyc 파일이 무엇인지, Python 반복문이 왜 느릴 수 있는지까지 연결해서 이해할 수 있다.

AST: 코드를 구조로 바꾸는 단계

AST는 Abstract Syntax Tree의 약자로, 우리말로는 추상 구문 트리라고 부른다. AST는 소스코드를 단순한 문자열이 아니라, 의미 있는 구조로 바꾼 것이다.

예를 들어 다음 코드가 있다.

x = a + b

사람은 이 코드를 보고 “a와 b를 더해서 x에 넣는구나”라고 이해한다. Python도 비슷한 과정을 거친다. 다만 Python은 이 코드를 트리 구조로 표현한다. Python에서는 ast 모듈로 이 구조를 직접 확인할 수 있다.

import ast

tree = ast.parse("x = a + b")
print(ast.dump(tree, indent=2))

출력은 대략 이런 형태다.

Module(
  body=[
    Assign(
      targets=[
        Name(id='x', ctx=Store())],
      value=BinOp(
        left=Name(id='a', ctx=Load()),
        op=Add(),
        right=Name(id='b', ctx=Load())))])

처음 보면 복잡해 보이지만 핵심은 단순하다. Python은 x = a + b라는 문자열을 보고, 이것을 “할당문”, “변수”, “덧셈 연산” 같은 의미 단위로 나눈다. 그리고 그 관계를 트리로 표현한다. AST는 Python 내부에서만 쓰이지 않는다. 개발 도구들도 AST를 적극적으로 사용한다.

Linter       : 코드 오류와 위험한 패턴 분석
Formatter    : 코드 스타일 정리
Transpiler   : 코드를 다른 형태로 변환
Static analyzer : 실행 전 코드 구조 분석

즉 AST는 “코드를 기계가 이해할 수 있는 구조로 바꾸는 단계”라고 볼 수 있다.

Bytecode: Python VM이 실행하는 중간 코드

AST가 만들어지면 Python은 이를 바이트코드로 컴파일한다. 바이트코드는 CPU가 직접 실행하는 기계어가 아니다. Python Virtual Machine이 이해할 수 있는 중간 명령어다.

import dis

def add(x, y):
    return x + y

dis.dis(add)

Python 3.11 이후 버전에서는 대략 다음과 비슷한 바이트코드를 볼 수 있다.

LOAD_FAST 0 (x)
LOAD_FAST 1 (y)
BINARY_OP 0 (+)
RETURN_VALUE

각 명령어의 의미는 이렇다.

LOAD_FAST    : 지역 변수 값을 스택에 올림
BINARY_OP    : 두 값을 꺼내 연산 수행
RETURN_VALUE : 결과 반환

즉 return x + y라는 한 줄의 코드는 내부적으로 여러 개의 바이트코드 명령어로 바뀐다. Python VM은 이 명령어들을 하나씩 읽고 실행한다. 이때 Python VM은 스택 기반으로 동작한다. 값을 스택에 올리고, 연산에 필요한 값을 꺼내 계산한 뒤, 결과를 다시 올리는 방식이다.

이 구조를 알면 Python 코드가 “마법처럼” 실행되는 것이 아니라, 꽤 명확한 단계를 거쳐 실행된다는 것을 알 수 있다.

__pycache__와 .pyc 파일의 정체

Python 프로젝트를 실행하다 보면 이런 디렉터리를 본 적이 있을 것이다.

__pycache__

그 안에는 보통 이런 파일이 들어 있다.

hello.cpython-311.pyc

.pyc 파일은 Python 소스코드를 컴파일한 바이트코드 파일이다.

직접 만들 수도 있다.

python -m py_compile hello.py

이 명령을 실행하면 hello.py가 바이트코드로 컴파일되고, __pycache__ 아래에 .pyc 파일이 생성된다. Python이 .pyc 파일을 저장하는 이유는 재사용성 때문이다. 이미 컴파일된 바이트코드를 다시 사용할 수 있으면, 매번 소스코드를 처음부터 파싱하고 컴파일하는 비용을 줄일 수 있다. 물론 .pyc가 있다고 해서 Python이 C/C++처럼 네이티브 기계어로 빨라지는 것은 아니다. .pyc는 어디까지나 Python VM이 실행하는 중간 코드다.

PVM: 바이트코드를 실행하는 엔진

PVM은 Python Virtual Machine의 약자다. 바이트코드를 읽고 실행하는 Python의 실행 엔진이라고 볼 수 있다. Python 실행 흐름을 다시 정리하면 다음과 같다.

.py 파일
→ Parser
→ AST
→ Bytecode
→ PVM
→ 결과

CPython은 소스코드를 직접 기계어로 바꾸지 않는다. 대신 바이트코드를 만들고, PVM이 이를 해석하며 실행한다. 이 방식의 장점은 이식성이다. Python 코드는 운영체제마다 완전히 다른 방식으로 작성하지 않아도 된다. Python 실행 환경만 있다면 같은 코드를 여러 환경에서 실행할 수 있다. 반면 단점도 있다. PVM이 바이트코드를 하나씩 해석하며 실행하기 때문에, 순수 Python 반복문은 C/C++ 같은 컴파일 언어보다 느릴 수 있다.

그래서 데이터 분석에서는 이런 코드보다

result = []

for x in values:
    result.append(x * 2)

가능하면 이런 식의 벡터화 연산을 선호한다.

result = values * 2

NumPy나 Pandas의 내부 연산은 많은 경우 C/C++로 구현되어 있다. Python은 분석 흐름을 제어하고, 실제 대량 연산은 최적화된 라이브러리에 맡기는 방식이다. 이것이 Python이 느린 언어임에도 데이터 분석에서 강력한 이유다.

Python 메모리 모델

Python에서는 모든 값이 객체다.

x = 10
name = "python"
items = [1, 2, 3]

여기서 10, "python", [1, 2, 3]은 모두 객체다.

Python 객체는 단순히 값만 들고 있지 않다. 내부적으로는 타입 정보, 참조 수, 실제 값 등의 정보를 가진다.

Python 객체
→ type 정보
→ reference count
→ value

메모리 관점에서 보면 Stack과 Heap을 나눠 생각할 수 있다.

Stack : 함수 호출 정보, 지역 변수 참조
Heap  : 실제 객체, 리스트, 딕셔너리, 클래스 인스턴스

Python은 메모리를 자동으로 관리한다. 대표적인 방식은 Reference Counting이다. 어떤 객체를 참조하는 곳이 몇 개인지 세고, 참조 수가 0이 되면 해당 객체를 해제할 수 있다. 하지만 Reference Counting만으로는 해결하기 어려운 경우가 있다. 예를 들어 객체 A와 객체 B가 서로를 참조하는 순환 참조가 생기면, 외부에서 더 이상 사용하지 않아도 참조 수가 0이 되지 않을 수 있다. 이런 경우를 처리하기 위해 Python은 Garbage Collector도 함께 사용한다.

실행 구조를 이해하면 코드가 다르게 보인다

Python의 실행 구조를 알면 몇 가지가 더 잘 보인다.

첫째, __pycache__가 왜 생기는지 이해할 수 있다.
Python은 소스코드를 바이트코드로 컴파일하고, 이를 재사용하기 위해 .pyc 파일을 저장한다.

둘째, Python이 왜 느릴 수 있는지 이해할 수 있다.
Python 코드는 PVM 위에서 바이트코드로 실행된다. 순수 Python 반복문이 많은 코드는 바이트코드 해석 비용이 커질 수 있다.

셋째, 왜 데이터 분석에서 벡터화 연산이 중요한지 이해할 수 있다.
Python 레벨의 반복을 줄이고, NumPy, Pandas, Polars 같은 라이브러리 내부의 최적화된 연산을 활용하는 것이 성능에 유리하다.

넷째, dis나 ast 같은 도구를 통해 Python 코드를 더 깊이 들여다볼 수 있다.
문법적으로 같은 일을 하는 코드라도 내부 바이트코드는 다를 수 있다. 이 차이가 성능 차이로 이어지는 경우도 있다.

마무리

Python은 흔히 인터프리터 언어라고 불린다. 하지만 CPython의 실제 실행 구조는 단순히 “한 줄씩 읽고 바로 실행”하는 것보다 정교하다. Python은 소스코드를 파싱하고, AST를 만들고, 바이트코드로 컴파일한 뒤, Python Virtual Machine에서 실행한다.

Source Code
→ Parser
→ AST
→ Bytecode
→ PVM
→ Result

이 구조 덕분에 Python은 높은 이식성과 생산성을 제공한다. 동시에 PVM 위에서 실행되기 때문에 순수 Python 코드의 성능 한계도 존재한다. 그래서 Python을 잘 쓴다는 것은 문법을 많이 아는 것만을 의미하지 않는다. Python이 어떤 방식으로 실행되는지 이해하고, 반복문을 어디까지 Python에게 맡길지, 어느 부분을 NumPy나 Pandas 같은 최적화된 라이브러리에 넘길지 판단하는 것도 중요하다. Python이 데이터 분석과 AI/ML의 표준 언어가 된 이유는 명확하다. 빠르게 실험할 수 있고, 읽기 쉬우며, 생태계가 압도적으로 크고, 최신 AI 개발 도구들이 Python을 중심으로 발전해왔기 때문이다. 하지만 한 단계 더 나아가려면 Python을 “쉬운 언어”로만 보면 부족하다. Python이 내부에서 코드를 어떻게 이해하고 실행하는지 알 때, 분석 코드의 성능과 구조를 더 정확하게 판단할 수 있다.

반응형