소마 프로젝트로 코드의 진행과정을 시각화 해주는 서비스를 만들기로 했다!
우선 간단한 별찍기 로직을 시각화 먼저 해보기 했다.
목표 코드
a = 8
for i in range(a):
print('*' * (i+1))
a = 8를 AST 트리로 변환 후 트리 출력하기
import ast
# 분석할 소스 코드
source_code = """
a = 8
"""
# 소스 코드를 AST로 파싱
parsed_ast = ast.parse(source_code)
# AST 트리를 출력
def print_ast(node, level=0):
print(' ' * level + ast.dump(node))
for child in ast.iter_child_nodes(node):
print_ast(child, level + 1)
print_ast(parsed_ast)
- ast 모듈을 임포트합니다.
- source_code 변수에 분석할 파이썬 소스 코드를 문자열로 저장합니다.
- ast.parse() 함수를 사용하여 소스 코드를 AST로 변환합니다.
- 재귀적으로 AST 노드를 순회하며 노드의 내용을 출력하는 print_ast 함수를 정의하고 호출합니다.
`ast.dump` 함수는 AST 노드를 문자열 형태로 반환하여 노드의 세부 내용을 출력합니다. `ast.iter_child_nodes` 함수는 주어진 노드의 자식 노드를 반복(iterate)할 수 있게 합니다.
콘솔 출력 결과
Module(body=[Assign(targets=[Name(id='a', ctx=Store())], value=Constant(value=8))], type_ignores=[])
Assign(targets=[Name(id='a', ctx=Store())], value=Constant(value=8))
Name(id='a', ctx=Store())
Store()
Constant(value=8)
ast.parse(source, filename='<unknown>', mode='exec', ***, type_comments=False, feature_version=None)
- 소스를 AST 노드로 구문 분석합니다.
- builtins.compile(source, filename, mode, ast.PyCF_ONLY_AST)와 동등합니다.
- Ast.parse()
- builtins.compile()
- source를 exec() 또는 eval()로 실행할 수 있는 code object로 컴파일한다.
a = 8를 AST 트리로 변환 후 트리 출력하기
import ast
# 분석할 소스 코드
source_code = """
a = 8
"""
# 소스 코드를 AST로 파싱
parsed_ast = ast.parse(source_code)
# AST 트리를 출력
def print_ast(node, level=0):
print(' ' * level + ast.dump(node))
for child in ast.iter_child_nodes(node):
print_ast(child, level + 1)
print_ast(parsed_ast)
- ast 모듈을 임포트합니다.
- source_code 변수에 분석할 파이썬 소스 코드를 문자열로 저장합니다.
- ast.parse() 함수를 사용하여 소스 코드를 AST로 변환합니다.
- 재귀적으로 AST 노드를 순회하며 노드의 내용을 출력하는 print_ast 함수를 정의하고 호출합니다.
`ast.dump`함수는 AST 노드를 문자열 형태로 반환하여 노드의 세부 내용을 출력합니다. `ast.iter_child_nodes` 함수는 주어진 노드의 자식 노드를 반복(iterate)할 수 있게 합니다.
콘솔 출력 결과
Module(body=[Assign(targets=[Name(id='a', ctx=Store())], value=Constant(value=8))], type_ignores=[])
Assign(targets=[Name(id='a', ctx=Store())], value=Constant(value=8))
Name(id='a', ctx=Store())
Store()
Constant(value=8)
ast.parse(source, filename='<unknown>', mode='exec', ***, type_comments=False, feature_version=None)
소스를 AST 노드로 구문 분석합니다.
builtins.compile(source, filename, mode, ast.PyCF_ONLY_AST)와 동등합니다.
- Ast.parse()
- builtins.compile()
source를 exec() 또는 eval()로 실행할 수 있는 code object로 컴파일한다.
Q&A
Question 1
의문1 : ast.parse 함수에서 compile을 호출할 때 5개의 매개 변수를 넘겨주는데 compile 함수를 보면 매개변수가 2개밖에 없음. 어떻게 적용되고 실행되는거지?
Answer 1
함수 정의에서 가변 인자(*args, **kwargs)를 사용하기 때문이다. ***args**와 ****kwargs**는 각각 위치 인자와 키워드 인자를 여러 개 받을 수 있게 한다.
가변 인자 (args, *kwargs)
- args: 임의의 개의 위치 인자를 튜플로 받습니다.
- *kwargs: 임의의 개수의 키워드 인자를 딕셔너리로 받습니다.
이를 통해 함수는 호출될 때 다양한 형태와 개수의 인자를 받을 수 있습니다.
compile 함수의 예
compile 함수의 실제 정의는 내부적으로 더 많은 인자를 받을 수 있게 설계되어 있습니다. 예를 들어, 다음과 같이 정의된 함수가 있다고 가정해봅시다:
def compile(*args, **kwargs):
print("args:", args)
print("kwargs:", kwargs)
이 함수는 호출 시 제공된 모든 위치 인자와 키워드 인자를 각각 **args**와 **kwargs**에 저장합니다. 예를 들어, 다음과 같이 호출할 수 있습니다:
compile("source", "filename", "mode", 123, _feature_version=10)
이 호출에서는 다음과 같이 출력됩니다:
args: ('source', 'filename', 'mode', 123)
kwargs: {'_feature_version': 10}
여기서 ***args**는 모든 위치 인자를 튜플로, ****kwargs**는 모든 키워드 인자를 딕셔너리로 받습니다.
Question 2
그러면 정의되지 않는 매개변수가 넘어올텐데 가변 인자를 매개변수로 사용하는 함수는 어떻게 예측을 하고 로직을 짜는거지?
Answer 2
가변 인자를 매개변수로 사용하는 함수는 일반적으로 다양한 호출 상황을 처리할 수 있도록 설계됩니다. ***args**와 ****kwargs**를 사용하는 함수는 유연하게 인자를 받을 수 있기 때문에, 함수 내부에서 어떤 인자가 넘어왔는지 확인하고 처리하는 로직을 작성해야 합니다. 이는 주로 다음과 같은 방식으로 이루어집니다:
예제: compile 함수
def my_function(*args, **kwargs):
# 기본값 설정
source = filename = mode = None
flags = 0
feature_version = -1
# 위치 인자 처리
if len(args) > 0:
source = args[0]
if len(args) > 1:
filename = args[1]
if len(args) > 2:
mode = args[2]
if len(args) > 3:
flags = args[3]
# 키워드 인자 처리
if 'source' in kwargs:
source = kwargs['source']
if 'filename' in kwargs:
filename = kwargs['filename']
if 'mode' in kwargs:
mode = kwargs['mode']
if 'flags' in kwargs:
flags = kwargs['flags']
if '_feature_version' in kwargs:
feature_version = kwargs['_feature_version']
# 유효성 검사
if source is None or filename is None or mode is None:
raise ValueError("source, filename, and mode are required arguments")
# 적절한 처리
print(f"Compiling source: {source}, filename: {filename}, mode: {mode}, flags: {flags}, feature_version: {feature_version}")
# 함수 호출 예제
my_function("source_code", "file.py", "exec", 123, _feature_version=10)
파이썬 소스 코드를 실행하는 과정
인터프리트 언어지만 정확하게는 하이브리드 언어이다.
왜냐하면 다음과 같은 특징이 있기 때문이다.
- 인터프티러가 바이트 코드로 변환한다 (인터프리터 언어)
- PVM(Python Virtual Machine)은 바이트 코드를 한줄 한줄 번역하여 프로그램 실행한다 (컴파일)
더 자세한 작동 방식은 다음과 같다
- Step 1: 파이썬 컴파일러가 소스 코드를 읽습니다. 그리고 나서 소스 코드가 잘 작성되었는지를 판단하기 위해 문법 오류 검사를 진행합니다. 만일 문법 오류가 발견되었다면 그 즉시 컴파일 과정을 멈추고 에러 메시지를 출력합니다.
- Step 2: 만일 에러가 발생하지 않았다면 컴파일러가 소스 코드를 바이트 코드로 변환합니다.
- Step 3: 마지막으로 바이트 코드는 PVM(Python Virtual Machine)에 보내집니다. PVM은 바이트 코드를 컴퓨터가 실행할 수 있는 기계어로 한줄 한줄 번역합니다. CPU는 기계어를 실행
GPT에게 물어본 파이썬이 소스코드를 컴파일하고 실행하는 과정
- 소스 코드 입력:
- 파이썬 인터프리터가 소스 코드를 입력받습니다. 이 코드는 파일에서 읽을 수도 있고, 인터프리터의 대화형 셸에서 직접 입력받을 수도 있습니다.
- 구문 분석 (Parsing):
- 소스 코드는 구문 분석기(parser)에 의해 토큰(token)으로 분리됩니다. 이 토큰들은 언어의 구문 규칙에 따라 분석됩니다.
- 파이썬은 이 단계에서 소스 코드를 추상 구문 트리(Abstract Syntax Tree, AST)로 변환합니다. 이 과정은 ast 모듈에서 제공하는 함수로 확인할 수 있습니다.
- 추상 구문 트리(AST) 생성:
- 구문 분석된 토큰들은 AST로 변환됩니다. AST는 소스 코드의 구조를 트리 형태로 표현한 것입니다.
- 각 노드는 소스 코드의 구조적 요소(예: 연산자, 제어 구조, 함수 정의 등)를 나타냅니다.
- 바이트코드 생성 (Compilation):
- AST는 바이트코드(bytecode)로 컴파일됩니다. 바이트코드는 파이썬 가상 머신(Python Virtual Machine, PVM)이 이해할 수 있는 일종의 중간 언어입니다.
- 컴파일된 바이트코드는 .pyc 파일로 저장될 수 있습니다.
compiled_code = compile(parsed_ast, filename="<ast>", mode="exec")
- 바이트코드 실행:
- 바이트코드는 파이썬 가상 머신(PVM)에 의해 실행됩니다. PVM은 바이트코드를 하나씩 읽고 실행합니다.
- 이 단계에서는 변수 할당, 함수 호출, 연산 수행 등 실제 프로그램의 동작이 이루어집니다.
Question 3
compile 함수를 호출해서 AST 트리를 바이트 코드로 변환한다. 하지만 아까 parse 함수 내부에서도 compile 함수를 호출하고 반환 값이 AST 트리인데? 다른 컴파일 함수인가?
Answer 3
우선 같은 builtins의 compile 함수가 맞다. 대신, parse 함수에서는 PyCF_ONLY_AST 플래그를 사용하여 컴파일합니다. 따라서, parse 함수의 반환값은 AST 노드이다.
- compile 함수의 flags 매개변수에 PyCF_ONLY_AST 플래그를 사용하면, 소스 코드가 파싱되고 AST가 생성됩니다. 즉, 바이트코드를 생성하지 않습니다.
- compile 함수의 반환값은 AST가 됩니다.
Question 4
바이트 코드는 PVM에서 실행되는데 그러면 모든 파이썬 코드는 PVm에서 실행되는건가? 기계어로 변환 후 CPU에서는 실행하지는 않나?
Answer 4
오직 PVM에서 바이트 코드가 실행된다. 따로 바이트 코드를 기계어로 변환하지 않는다. 파이썬 프로그램이 실행되는 동안, 코드의 실행은 파이썬 가상 머신에 의해 관리 된다. CPU는 PVM에 의해사용 될 뿐이다.
import argparse
import ast
# 테스트 코드
source_code = """
a = 8 + 10
b = a + 11
for i in range(a):
print('*' * (i+1))
"""
# AST 트리를 출력
def print_ast(node, level=0):
print(' ' * level + ast.dump(node))
for child in ast.iter_child_nodes(node):
print_ast(child, level + 1)
def visualize_code(code):
# 코드를 파싱하여 AST 생성
parsed_ast = ast.parse(code)
# 코드 실행을 위한 전역 및 지역 변수 저장소
global_vars = {}
local_vars = {}
# AST 노드를 순회하면서 실행
for node in parsed_ast.body:
# 변수 선언 및 할당 처리
if isinstance(node, ast.Assign):
for target in node.targets:
var_name = target.id
value = eval(compile(ast.Expression(node.value), '', 'eval'), global_vars, local_vars)
local_vars[var_name] = value
print(f"{var_name} = {value}")
# 반복문 처리
elif isinstance(node, ast.For):
iter_range = range(eval(compile(ast.Expression(node.iter), '', 'eval'), global_vars, local_vars))
for i in iter_range:
local_vars[node.target.id] = i
exec(compile(ast.Module(node.body), '', 'exec'), global_vars, local_vars)
# 소스 코드를 AST로 파싱
parsed_ast = ast.parse(source_code)
print("AST 구조:")
print_ast(parsed_ast)
visualize_code(source_code)
별찍기 코드를 AST 트리로 변환 후 트리 출력하기
import ast
# 분석할 소스 코드
source_code = """
a = 8
for i in range(a):
print('*' * (i+1))
"""
# 소스 코드를 AST로 파싱
parsed_ast = ast.parse(source_code)
# AST 트리를 출력
def print_ast(node, level=0):
print(' ' * level + ast.dump(node))
for child in ast.iter_child_nodes(node):
print_ast(child, level + 1)
print("AST 구조:")
print_ast(parsed_ast)
콘솔 출력 결과
AST 구조:
Module(body=[Assign(targets=[Name(id='a', ctx=Store())], value=Constant(value=8)), For(target=Name(id='i', ctx=Store()), iter=Call(func=Name(id='range', ctx=Load()), args=[Name(id='a', ctx=Load())], keywords=[]), body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[BinOp(left=Constant(value='*'), op=Mult(), right=BinOp(left=Name(id='i', ctx=Load()), op=Add(), right=Constant(value=1)))], keywords=[]))], orelse=[])], type_ignores=[])
Assign(targets=[Name(id='a', ctx=Store())], value=Constant(value=8))
Name(id='a', ctx=Store())
Store()
Constant(value=8)
For(target=Name(id='i', ctx=Store()), iter=Call(func=Name(id='range', ctx=Load()), args=[Name(id='a', ctx=Load())], keywords=[]), body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[BinOp(left=Constant(value='*'), op=Mult(), right=BinOp(left=Name(id='i', ctx=Load()), op=Add(), right=Constant(value=1)))], keywords=[]))], orelse=[])
Name(id='i', ctx=Store())
Store()
Call(func=Name(id='range', ctx=Load()), args=[Name(id='a', ctx=Load())], keywords=[])
Name(id='range', ctx=Load())
Load()
Name(id='a', ctx=Load())
Load()
Expr(value=Call(func=Name(id='print', ctx=Load()), args=[BinOp(left=Constant(value='*'), op=Mult(), right=BinOp(left=Name(id='i', ctx=Load()), op=Add(), right=Constant(value=1)))], keywords=[]))
Call(func=Name(id='print', ctx=Load()), args=[BinOp(left=Constant(value='*'), op=Mult(), right=BinOp(left=Name(id='i', ctx=Load()), op=Add(), right=Constant(value=1)))], keywords=[])
Name(id='print', ctx=Load())
Load()
BinOp(left=Constant(value='*'), op=Mult(), right=BinOp(left=Name(id='i', ctx=Load()), op=Add(), right=Constant(value=1)))
Constant(value='*')
Mult()
BinOp(left=Name(id='i', ctx=Load()), op=Add(), right=Constant(value=1))
Name(id='i', ctx=Load())
Load()
Add()
Constant(value=1)
/* 나올만한 node요소
Assign : 할당
Name.id : 변수 이름
Constant.value : 값 가져오기
Call : 함수 호출
*/
- AST 트리를 파이썬 코드로 변환하기
- ast.parse() 함수를 사용하여 소스 코드를 AST로 변환합니다.
- print_ast 함수는 AST 노드를 재귀적으로 탐색하며 트리 구조를 출력합니다.
- convert_ast_to_source 함수는 AST를 다시 파이썬 소스 코드 형태로 변환하여 출력합니다.
'Soma' 카테고리의 다른 글
MSA 구조를 Monolithic 구조로 변경하기 #2: ECR, EC2 배포하기 (0) | 2024.12.23 |
---|---|
MSA 구조를 Monolithic 구조로 변경하기 #1: EC2 생성과 연결 (0) | 2024.12.23 |
pytest.fixture를 이용한 테스트 (0) | 2024.06.14 |
2024년 소프트웨어 마에스트로 15기 합격 후기 (8) | 2024.03.30 |
2024년 소프트웨어 마에스트로 15기 면접 후기 (0) | 2024.03.16 |