차곡차곡 성 쌓기
Published 2024. 5. 18. 18:51
#1 별찍기 시각화 시도 Soma

소마 프로젝트로 코드의 진행과정을 시각화 해주는 서비스를 만들기로 했다!

우선 간단한 별찍기 로직을 시각화 먼저 해보기 했다.

 

 

목표 코드

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)

  1. ast 모듈을 임포트합니다.
  2. source_code 변수에 분석할 파이썬 소스 코드를 문자열로 저장합니다.
  3. ast.parse() 함수를 사용하여 소스 코드를 AST로 변환합니다.
  4. 재귀적으로 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()

ast — Abstract Syntax Trees

  • 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)

  1. ast 모듈을 임포트합니다.
  2. source_code 변수에 분석할 파이썬 소스 코드를 문자열로 저장합니다.
  3. ast.parse() 함수를 사용하여 소스 코드를 AST로 변환합니다.
  4. 재귀적으로 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()

ast — Abstract Syntax Trees

  • 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에게 물어본 파이썬이 소스코드를 컴파일하고 실행하는 과정

  1. 소스 코드 입력:
    • 파이썬 인터프리터가 소스 코드를 입력받습니다. 이 코드는 파일에서 읽을 수도 있고, 인터프리터의 대화형 셸에서 직접 입력받을 수도 있습니다.
  2. 구문 분석 (Parsing):
    • 소스 코드는 구문 분석기(parser)에 의해 토큰(token)으로 분리됩니다. 이 토큰들은 언어의 구문 규칙에 따라 분석됩니다.
    • 파이썬은 이 단계에서 소스 코드를 추상 구문 트리(Abstract Syntax Tree, AST)로 변환합니다. 이 과정은 ast 모듈에서 제공하는 함수로 확인할 수 있습니다.
  3. 추상 구문 트리(AST) 생성:
    • 구문 분석된 토큰들은 AST로 변환됩니다. AST는 소스 코드의 구조를 트리 형태로 표현한 것입니다.
    • 각 노드는 소스 코드의 구조적 요소(예: 연산자, 제어 구조, 함수 정의 등)를 나타냅니다.
  4. 바이트코드 생성 (Compilation):
    • AST는 바이트코드(bytecode)로 컴파일됩니다. 바이트코드는 파이썬 가상 머신(Python Virtual Machine, PVM)이 이해할 수 있는 일종의 중간 언어입니다.
    • 컴파일된 바이트코드는 .pyc 파일로 저장될 수 있습니다.
    compiled_code = compile(parsed_ast, filename="<ast>", mode="exec")
    
    
  5. 바이트코드 실행:
    • 바이트코드는 파이썬 가상 머신(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 트리를 파이썬 코드로 변환하기
    1. ast.parse() 함수를 사용하여 소스 코드를 AST로 변환합니다.
    2. print_ast 함수는 AST 노드를 재귀적으로 탐색하며 트리 구조를 출력합니다.
    3. convert_ast_to_source 함수는 AST를 다시 파이썬 소스 코드 형태로 변환하여 출력합니다.
728x90

'Soma' 카테고리의 다른 글

pytest.fixture를 이용한 테스트  (0) 2024.06.14
profile

차곡차곡 성 쌓기

@nagrang

포스팅이 좋았다면 "좋아요" 해주세요!