파이썬 컴파일러 활용하여 If 구문 가져오기

게으른 개미개발자·2022년 10월 13일
0

model_conversion

목록 보기
7/13

시작하며

‘python compiler를 활용해보아라’

‘token을 활용해보아라’

솔직히 어떤 의미인지 하나도 와닿지 않았다.

파이썬은 인터프리터 언어이기도하고, 파이썬으로 컴파일러를 구성하면서 코드 플로우를 따라가는 것에 이해가 잘 되지 않았다.

우선, 파이썬이라는 언어는 많이 쓰이는데 그에 대한 유래는 알지 못했다.

Python은 1991년 프로그래머인 귀도 반 로섬(Guido van Rossum)이 발표한 고급 프로그래밍 언어로, 플랫폼 독립적이며 인터프리터식, 객체지향적, 동적 타이핑(dynamically typed) 대화형 언어이다. 파이썬이라는 이름은 귀도가 좋아하는 코미디 〈Monty Python’s Flying Circus〉에서 따온 것이다.

출처 : wikipedia

그렇다면 파이썬은 어떻게 구동되는 것일까?

파이썬의 구현체는 CPython이다.

일반적으로 파이썬은 C로 wrapping되어있다고 알고 있다. 다시 말하자면 구현체가 CPython인 것이다.

CPython은 인터프리터이며 컴파일러이다. Python으로 코드를 작성하면 다음과 같은 과정을 거쳐 코드가 실행되게 된다.

최대한 간단하게 표현을 하면, 아래와 같다.

  1. CPython 컴파일러를 활용하여 Bytecode로 컴파일한다.
  2. CPython Virtual Machine(인터프리터)을 활용하여 실행한다.

CPython 외에도 JIT 컴파일을 도입한 PyPY, anaconda에서 만든 Numba, JVM에서 실행할 수 있는 Jython이 존재한다.

PyPy와 Numba의 경우, 요새 많이 사용하고 있는 컴파일러이기 때문에, 추후에 공부해 볼 필요가 있다.

코드에서 실행되는 IF문 찾기?

코드에서 if문을 찾는 것은 상당히 쉽다.

if money:
   print("택시를 타고 가라")
else:
   print("걸어 가라")

위와 같은 코드가 존재한다고 할 때, 직관적으로 if문이 어디있는지 알 수 있다.

근데, 만약 if문을 터미널과 같은 환경에서 어디에 있으며, if문에서 사용되는 파라미터가 무엇인지 찾아서 출력하려면 어떻게 해야될까?

왜 이런 고민을 하고있는지는 ‘forward 함수 내부 파악하기’ 포스팅을 읽어보면 이해가 될거에요.!

제일 먼저 떠오르는 방법은

  1. inspect.getsource() 메서드를 활용하여 코드 파싱하기
  2. 정규식을 활용하여 코드 파싱하기
  3. 또다른 방법(?)

위의 1번,2번 방법의 경우, 바로 떠오르기는 하지만, case가 다양해지거나, 문자열이 복잡해질 경우, 쉽지 않을 것이다.

그래서 결국 박사님께 힌트를 받아서 얻어낸 키워드는 ‘token’ 과 ‘compiler’였다.

구글링으로 ‘파이썬 컴파일러’를 검색해보았는데, 사람들이 실행해본 결과들이 있었고, 사실 가장 많이 도움을 받은 곳은 유튜브였다. 어느 분이 파이콘 레퍼런스에서 직접 컴파일러를 만들어서 발표하는 자료인데, 정말 대단하다. 어떻게 학부생 때, 저런 생각을 했지…? 아무튼 처음부터 끝까지 보면 정말 도움이 되는 영상이다. 나중에 다시 찾아볼 것 같다.

https://www.youtube.com/watch?v=dIqXpOAGL3M&t=1825s

영상을 쭉 보고 참고한 방법론은 아래 부분이다.

커스터마이징한 파이썬 컴파일러를 이런식으로 풀어서 설명해주었다.

실제로 저렇게 컴파일러가 존재하는 것이 아닌데, 기본적인 라이브러리를 활용하여 컴파이러를 구성하였다.

내가 IF문을 코드에서 찾기 위해서 필요한 부분은 ‘LEXER’와 ‘AST’라고 생각이 들었다.

  1. LEXER를 활용하여 IF문이 코드의 몇 번째 줄에 있는지 파악한다.
  2. AST를 활용하여 IF문 내부 구조를 파악한다.
    1. 트리를 순회하며 읽는다.

LEXER 부분을 활용하여 IF문의 위치를 찾는 것은 이전에 forward 함수 파라미터를 구한 코드와 합쳐서 같이 구현을 할 예정이다.

따라서 AST를 활용하여 IF문 내부 구조를 파악해보려고한다.

AST란

AST(Abstract Syntax Tree)란 단어 뜻 그대로, 구문이 축약되어 있는 트리이다. 즉, 구문을 분석한 트리라고할 수 있다.

AST가 파이썬 기본 라이브러리로 구현이 되어있기도 하다.

파이썬 ast는 다음과 같다.

ast 모듈은 파이썬 응용 프로그램이 파이썬 추상 구문 문법의 트리를 처리하는 데 도움을 줍니다. 추상 구문 자체는 각 파이썬 릴리스마다 바뀔 수 있습니다. 이 모듈은 프로그래밍 방식으로 현재 문법의 모양을 찾는 데 도움이 됩니다.
ast.PyCF_ONLY_AST를 플래그로 compile() 내장 함수에 전달하거나, 이 모듈에서 제공된 parse() 도우미를 사용하여 추상 구문 트리를 생성할 수 있습니다. 결과는 클래스가 모두 ast.AST에서 상속되는 객체들의 트리가 됩니다. 내장 compile() 함수를 사용하여 추상 구문 트리를 파이썬 코드 객체로 컴파일할 수 있습니다.

출처 : https://docs.python.org/ko/3/library/ast.html#literals

또한, 레퍼런스를 확인해보면 추상 문법이 있는데, 추상 문법을 활용하여, 구문(코드)에서 내가 원하는 부분을 순회할 수 있으며, 기록할 수 있고, 컴파일러를 만들기 위해 code generator의 기반이 될 수도 있다.

이번 시간에는 IF문의 위치만을 찾는 것이기에, IF문 관련 문법들만 활용해보려고한다.

class CodeAnalyzer(NodeVisitor):
    def __init__(self):
        self.stats = {'import':[],'class':[],'function':[]}
    
    def visit_Import(self,node):
        for alias in node.names:
            self.stats['import'].append(alias.name)
    
    def visit_ClassDef(self,node):
        self.stats['class'].append(node.name)
        self.generic_visit(node)
    
    def visit_FunctionDef(self, node):
        self.stats['function'].append(node.name)
        self.generic_visit(node)

기본적인 틀은 샘플코드를 참고하였다.

우선, ast.NodeVisitor가 필요하다. 말 그대로 노드를 순회할 수 있는 모듈이며, 이를 상속받는 CodeAnalyzer클래스를 만들어주었다.

해당 클래스안에서 visit해주려는 타입을 함수별로 정의하였다. 해당 함수들은 파이썬 레퍼런스의 ‘추상문법’에서 확인하여 갖다 쓸 수 있다.

def main():
		node = ast.parse(sourcecode)
    code_analyzer = CodeAnalyzer()
    code_analyzer.visit(node)
    code_analyzer.report()

if __name__ == "__main__":
    main()

main함수 안에서 ast의 parser를 활용하여 소스코드를 읽어오고, 인스턴스의 visit함수를 호출한다. 그면 ast의 NodeVisitor의 visit이 수행되고 node를 따라가면 visit_XXXX가 수행되게 된다.

self.generic_visit(node)

또한, 내부 코드를 계속 순회하기 위해서는 계속해서 node를 타고 들어가야되는데, 위의 self.generic_visit() 을 명시적으로 호출해주면서, 노드의 자식노드에 대해서 visit()을 호출하게 된다.

IF 구문안에 사용되는 파라미터의 종류별로 또 다르게 분기를 처리해주었으며, 코드는 아래와 같다.

hello.py라는 함수가 존재하는 코드가 있을 때,

import re

def say_hello(chk,chk2,chk3,chk4):
    chk_list=[0,1,2,3,4,5]
    chk_var1 = 5
    chk_var2 = 7
    chk_right = True
    if chk and chk2:
        print('Hello World')
    elif chk3 is not None:
        print('Happy Hacking')
    elif chk == chk_right:
        print('Coding makes world happy')
    elif chk4 >= 10000:
        print('millionaire')
    elif 2 in chk_list:
        print('list includes 2')
    elif chk_var1 + chk_var2:
        print('check Bin OP!')
    elif re.sub('apple|orange', 'fruit', 'apple box orange tree'):
        print('fruit box fruit tree')

ast_parser.py 코드를 활용하여, 코드를 순회하고 기록했다.

import ast
from pprint import pprint
import inspect
import sys
import os
sys.path.append(os.getcwd())
import hello

class CodeAnalyzer(ast.NodeVisitor):
    def __init__(self):
        self.stats = {'import':[],'class':[],'function':[],'if':[]}
    
    def visit_Import(self,node):
        for alias in node.names:
            self.stats['import'].append(alias.name)
    
    def visit_ClassDef(self,node):
        self.stats['class'].append(node.name)
        self.generic_visit(node)
    
    def visit_FunctionDef(self, node):
        self.stats['function'].append(node.name)
        self.generic_visit(node)

    def if_Name(self,node):
        self.stats['if'].append(node.id)

    def if_Bool(self,node):
        node_values = node.values
        for values in node_values:
            self.stats['if'].append(values.id)

    def if_Compare(self,node):
        left_node = node.left
        comparators_node = node.comparators
        if isinstance(left_node,ast.Name):
            self.stats['if'].append(left_node.id)
        for idx in range(len(comparators_node)):
            if isinstance(comparators_node[idx],ast.Name):
                self.stats['if'].append(comparators_node[idx].id)

    def if_BinOp(self,node):
        left_node = node.left
        right_node = node.right
        if isinstance(left_node,ast.Name):
            self.stats['if'].append(left_node.id)
        elif isinstance(right_node,ast.Name):
            self.stats['if'].append(right_node.id)

    def if_Call(self,node):
        func_node = node.func
        if isinstance(func_node,ast.Attribute):
            value_node = func_node.value
            self.stats['if'].append(value_node.id)

    def visit_If(self,node):
        test_node = node.test
        if isinstance(test_node,ast.Name):
            self.if_Name(test_node)
        
        elif isinstance(test_node,ast.BoolOp):
            self.if_Bool(test_node)
    
        elif isinstance(test_node,ast.Compare):
            self.if_Compare(test_node)

        elif isinstance(test_node,ast.BinOp):
            self.if_BinOp(test_node)

        elif isinstance(test_node,ast.Call):
            self.if_Call(test_node)

        self.generic_visit(node)

    def report(self):
        pprint(self.stats)
    

def main():
    code_hello = hello.say_hello
    sourcecode = inspect.getsource(code_hello)
    node = ast.parse(sourcecode)

    code_analyzer = CodeAnalyzer()
    code_analyzer.visit(node)
    code_analyzer.report()

if __name__ == "__main__":
    main()

if문에서 사용되는 파라미터들을 리스트에 담아주기 때문에, 중복값이 담기게 되는데, 추후 forward 파라미터를 체크한 모듈과 병합하여, 하나의 패키지로 구성해 처리할 것이다.

profile
특 : 미친듯한 게으름과 부지런한 생각이 공존하는 사람

0개의 댓글