Execution Model
Python 실행 모델은 "인터프리터가 코드를 한 줄씩 읽는다"보다 훨씬 구체적이다. source를 AST와 code object로 바꾸고, frame 위에서 bytecode를 평가한다는 감각이 있어야 import, closure, late binding, 디버깅 포인트가 한 번에 정리된다.
빠른 요약: Python은 source를 AST와 code object로 바꾼 뒤, frame 안의 namespace와 evaluation stack을 사용해 bytecode를 실행한다. import, scope, closure 문제는 대부분 이 그림에서 풀린다.
먼저 머리에 넣을 그림
왜 중요한가
- circular import가 왜 생기는지 import 시점으로 설명할 수 있다.
- closure와 late binding이 왜 생기는지 scope와 cell object로 이해할 수 있다.
dis, traceback, profiler 출력이 훨씬 자연스럽게 읽힌다.
실행 흐름을 4단계로 보기
1. Parse와 compile
- parser는 source를 AST로 바꾼다.
- compiler는 AST를 code object로 만든다.
- code object는 실행 가능한 명령 묶음이지, 아직 실행 중인 상태는 아니다.
2. 이름 바인딩과 scope 결정
- compiler는 어떤 이름이 local, free, cell, global인지 미리 계산한다.
- 그래서 closure는 "나중에 얼추 찾아본다"가 아니라, 특정 scope 슬롯을 가리키는 구조로 동작한다.
3. frame 생성
- 함수 호출, 모듈 import, comprehension 같은 실행 단위는 frame을 만든다.
- frame에는 locals, globals, builtins, instruction pointer, evaluation stack 같은 실행 문맥이 들어 있다.
4. eval loop가 bytecode 실행
- CPython은 code object 안의 bytecode를 opcode 단위로 실행한다.
- attribute lookup, function call, exception handling도 결국 이 평가 루프 위에서 돌아간다.
코드로 보면 더 빨리 잡히는 것
py
import dis
source = """
x = 10
def outer(y):
z = x + y
def inner():
return z * 2
return inner
"""
module_code = compile(source, "demo.py", "exec")
namespace: dict[str, object] = {}
exec(module_code, namespace)
outer = namespace["outer"]
inner = outer(5)
print("module code:", module_code)
print("outer closure vars:", outer.__code__.co_freevars)
print("inner closure vars:", inner.__code__.co_freevars)
dis.dis(outer)
dis.dis(inner)이 예제에서 `compile()`은 code object를 만들고, `exec()`가 namespace를 채운다. `inner`는 `z`를 free variable로 잡고 있어서 closure cell을 통해 값에 접근한다.
실무에서 특히 도움이 되는 순간
Import cycle
모듈 import도 frame에서 실행되므로, "아직 다 초기화되지 않은 모듈"을 참조하는 순간 순환 참조 문제가 드러난다.
Late binding
루프 안 람다나 closure가 마지막 값을 보는 이유를 cell object와 scope 계산으로 설명할 수 있다.
Profiler output
frame과 opcode 단위를 알면 traceback, `dis`, profiler 결과가 단순 문자열이 아니라 실행 모델로 읽힌다.
체크리스트
- import 시점 부작용이 큰 모듈은 초기화 순서를 의식해서 나눈다.
- closure 버그가 의심되면 free variable과 default argument 캡처를 먼저 본다.
- 런타임 문제를 볼 때는 source만 보지 말고 code object, frame, bytecode까지 생각한다.