수색…


소개

PLY는 인기있는 컴파일러 생성 도구 lex 및 yacc의 순수 Python 구현입니다.

비고

추가 링크 :

  1. 공식 문서
  2. 기생충

PLY 시작하기

Python2 / 3 용으로 PLY를 설치하려면 아래에 설명 된 단계를 따르십시오.

  1. 여기 에서 소스 코드를 다운로드 하십시오 .
  2. 다운로드 한 zip 파일의 압축을 풉니 다.
  3. 압축을 푼 ply-3.10 폴더로 이동하십시오.
  4. 터미널에서 다음 명령을 실행하십시오 : python setup.py install

위의 작업을 완료했다면 이제 PLY 모듈을 사용할 수 있습니다. 파이썬 인터프리터를 열고 import ply.lex 입력하여 테스트해볼 수 있습니다.

참고 : 사용하지 마십시오 pip PLY를 설치하기 위해서는 컴퓨터에 깨진 분포를 설치합니다.

"안녕, 세상!" 의 PLY - 간단한 계산기

간단한 예제를 통해 PLY의 힘을 설명해 보겠습니다.이 프로그램은 산술 식을 문자열 입력으로 사용하여이를 해결하려고 시도합니다.

좋아하는 편집기를 열고 다음 코드를 복사하십시오.

from ply import lex
import ply.yacc as yacc

tokens = (
    'PLUS',
    'MINUS',
    'TIMES',
    'DIV',
    'LPAREN',
    'RPAREN',
    'NUMBER',
)

t_ignore = ' \t'

t_PLUS    = r'\+'
t_MINUS   = r'-'
t_TIMES   = r'\*'
t_DIV     = r'/'
t_LPAREN  = r'\('
t_RPAREN  = r'\)'

def t_NUMBER( t ) :
    r'[0-9]+'
    t.value = int( t.value )
    return t

def t_newline( t ):
  r'\n+'
  t.lexer.lineno += len( t.value )

def t_error( t ):
  print("Invalid Token:",t.value[0])
  t.lexer.skip( 1 )

lexer = lex.lex()

precedence = (
    ( 'left', 'PLUS', 'MINUS' ),
    ( 'left', 'TIMES', 'DIV' ),
    ( 'nonassoc', 'UMINUS' )
)

def p_add( p ) :
    'expr : expr PLUS expr'
    p[0] = p[1] + p[3]

def p_sub( p ) :
    'expr : expr MINUS expr'
    p[0] = p[1] - p[3]

def p_expr2uminus( p ) :
    'expr : MINUS expr %prec UMINUS'
    p[0] = - p[2]

def p_mult_div( p ) :
    '''expr : expr TIMES expr
            | expr DIV expr'''

    if p[2] == '*' :
        p[0] = p[1] * p[3]
    else :
        if p[3] == 0 :
            print("Can't divide by 0")
            raise ZeroDivisionError('integer division by 0')
        p[0] = p[1] / p[3]

def p_expr2NUM( p ) :
    'expr : NUMBER'
    p[0] = p[1]

def p_parens( p ) :
    'expr : LPAREN expr RPAREN'
    p[0] = p[2]

def p_error( p ):
    print("Syntax error in input!")

parser = yacc.yacc()

res = parser.parse("-4*-(3-5)") # the input
print(res)

이 파일을 calc.py 로 저장하고 실행하십시오.

산출:

-8

-4 * - (3 - 5) 대한 올바른 대답은 무엇입니까?

파트 1 : Lex로 입력 토큰 화하기

예제 1의 코드가 수행 한 두 단계는 입력을 토큰 화 하는 것입니다. 즉, 산술 표현식을 구성하는 기호를 찾는 것이고 두 번째 단계는 추출 된 토큰을 분석하고 결과를 평가하는 파싱 입니다.

이 절에서는 사용자 입력을 토큰 화 한 다음 줄 단위로 구분하는 간단한 예제를 제공합니다.

    import ply.lex as lex

    # List of token names. This is always required
    tokens = [
       'NUMBER',
       'PLUS',
       'MINUS',
       'TIMES',
       'DIVIDE',
       'LPAREN',
       'RPAREN',
    ]

    # Regular expression rules for simple tokens
    t_PLUS    = r'\+'
    t_MINUS   = r'-'
    t_TIMES   = r'\*'
    t_DIVIDE  = r'/'
    t_LPAREN  = r'\('
    t_RPAREN  = r'\)'

    # A regular expression rule with some action code
    def t_NUMBER(t):
        r'\d+'
        t.value = int(t.value)    
        return t

    # Define a rule so we can track line numbers
    def t_newline(t):
        r'\n+'
        t.lexer.lineno += len(t.value)

    # A string containing ignored characters (spaces and tabs)
    t_ignore  = ' \t'

    # Error handling rule
    def t_error(t):
        print("Illegal character '%s'" % t.value[0])
        t.lexer.skip(1)

    # Build the lexer
    lexer = lex.lex()

    # Give the lexer some input
    lexer.input(data)

    # Tokenize
    while True:
        tok = lexer.token()
        if not tok: 
            break      # No more input
        print(tok)

이 파일을 calclex.py 로 저장하십시오. 우리는 Yacc 파서를 만들 때 이것을 사용할 것입니다.


고장

  1. import ply.lex 사용하여 모듈 import ply.lex

  2. 모든 렉서는 렉서가 생성 할 수있는 모든 가능한 토큰 이름을 정의하는 tokens 이라고하는 목록을 제공해야합니다. 이 목록은 항상 필요합니다.

     tokens = [
        'NUMBER',
        'PLUS',
        'MINUS',
        'TIMES',
        'DIVIDE',
        'LPAREN',
        'RPAREN',
     ]
    

tokens 은 문자열이 아닌 문자열의 튜플 일 수도 있습니다. 각 문자열은 이전과 같이 토큰을 나타냅니다.

  1. 각 문자열에 대한 정규식 규칙은 문자열 또는 함수로 정의 할 수 있습니다. 두 경우 모두, 변수 이름 앞에는 토큰을 일치시키는 규칙임을 나타내는 t_가 붙어야합니다.

    • 간단한 토큰의 경우 정규 표현식을 문자열로 지정할 수 있습니다. t_PLUS = r'\+'

    • 일종의 작업을 수행해야하는 경우 토큰 규칙을 함수로 지정할 수 있습니다.

         def t_NUMBER(t):
             r'\d+'
             t.value = int(t.value)
             return t
      

      규칙은 함수 내에서 문서 문자열로 지정됩니다. 이 함수는 LexToken 의 인스턴스 인 하나의 인수를 받아들이고 일부 동작을 수행 한 다음 인수를 반환합니다.

      문서 문자열을 지정하는 대신 함수의 정규식 규칙으로 외부 문자열을 사용하려면 다음 예제를 고려하십시오.

         @TOKEN(identifier)         # identifier is a string holding the regex
         def t_ID(t):
             ...      # actions
      
    • LexToken 객체 (이 객체를 t 라고 부름)의 인스턴스에는 다음과 같은 속성이 있습니다.

      1. t.type 은 토큰 유형 (문자열)입니다 (예 : 'NUMBER' , 'PLUS' 등). 기본적으로 t.typet_ 접두어 다음의 이름으로 설정됩니다.
      2. 어휘 (실제 텍스트가 일치 함) 인 t.value
      3. 현재 라인 번호 인 t.lineno (이것은 렉서가 라인 번호를 전혀 t.lineno 때문에 자동으로 업데이트되지 않는다). t_newline 이라는 함수를 사용하여 lineno를 업데이트 t_newline .

        def t_newline(t):
            r'\n+'
            t.lexer.lineno += len(t.value)
      

      1. t.lexpos 는 입력 텍스트의 시작 부분에 상대적인 토큰의 위치입니다.
    • regex 규칙 함수에서 아무 것도 반환되지 않으면 토큰이 삭제됩니다. 토큰을 버리려면 동일한 규칙에 대한 함수를 정의하는 대신 t_ignore_ 접두사를 정규식 규칙 변수에 추가 할 수 있습니다.

         def t_COMMENT(t):
             r'\#.*'
             pass
             # No return value. Token discarded
      

      ...와 같다:

         t_ignore_COMMENT = r'\#.*'
      

      댓글을 볼 때 어떤 조치를 취하는 경우 이는 물론 잘못된 것입니다. 어떤 경우에는 함수를 사용하여 정규식 규칙을 정의하십시오.

      일부 문자에 대한 토큰을 정의하지 않았지만 여전히 무시하려면 t_ignore = "<characters to ignore>" (이 접두어는 필수입니다).

         t_ignore_COMMENT = r'\#.*'
         t_ignore  = ' \t'    # ignores spaces and tabs
      

    • 마스터 정규 표현식을 만들 때, lex는 다음과 같이 파일에 지정된 정규 표현식을 추가합니다.

      1. 함수로 정의 된 토큰은 파일에 표시된 것과 동일한 순서로 추가됩니다.
      2. 문자열로 정의 된 토큰은 해당 토큰의 정규식을 정의하는 문자열의 문자열 길이가 감소하는 순서로 추가됩니다.

      동일한 파일에서 === 과 일치하는 == 규칙을 활용하십시오.

    • 리터럴은 반환되는 토큰입니다. t.typet.value 모두 문자 자체로 설정됩니다. 다음과 같이 리터럴 목록을 정의하십시오.

      literals = [ '+', '-', '*', '/' ]
      

      또는,

      literals = "+-*/"
      

      리터럴이 일치 할 때 추가 작업을 수행하는 토큰 함수를 작성할 수 있습니다. 그러나 토큰 유형을 적절하게 설정해야합니다. 예 :

      literals = [ '{', '}' ]
      
      def t_lbrace(t):
          r'\{'
          t.type = '{'  # Set token type to the expected literal (ABSOLUTE MUST if this is a literal)
          return t
      

    • t_error 함수로 오류를 처리하십시오.

      # Error handling rule
      def t_error(t):
          print("Illegal character '%s'" % t.value[0])
          t.lexer.skip(1) # skip the illegal token (don't process it)
      

      일반적으로 t.lexer.skip(n) 은 입력 문자열에서 n 문자를 건너 뜁니다.

  2. 최종 준비 :

    lexer = lex.lex() 사용하여 lexer = lex.lex() 빌드하십시오.

    모든 것을 클래스 안에 넣고 클래스의 인스턴스를 호출하여 렉서를 정의 할 수도 있습니다. 예 :

     import ply.lex as lex  
     class MyLexer(object):            
           ...     # everything relating to token rules and error handling comes here as usual 
    
           # Build the lexer
           def build(self, **kwargs):
               self.lexer = lex.lex(module=self, **kwargs)
    
           def test(self, data):
               self.lexer.input(data)
               for token in self.lexer.token():
                   print(token)
    
           # Build the lexer and try it out
    
     m = MyLexer()
     m.build()           # Build the lexer
     m.test("3 + 4")     #
    

    데이터가 문자열 인 lexer.input(data) 사용하여 입력 제공

    토큰을 얻으려면 일치하는 토큰을 반환하는 lexer.token() 을 사용 lexer.token() . 다음과 같이 루프에서 렉서를 반복 할 수 있습니다.

    for i in lexer: 
        print(i)
    

2 부 : Yacc로 토큰 화 된 입력 분석

이 섹션에서는 Part 1의 토큰 화 된 입력을 처리하는 방법을 설명합니다. 이는 컨텍스트 프리 그래머 (CFG)를 사용하여 수행됩니다. 문법을 지정해야하며 토큰은 문법에 따라 처리됩니다. 후드 아래에서 파서는 LALR 파서를 사용합니다.

# Yacc example

import ply.yacc as yacc

# Get the token map from the lexer. This is required.
from calclex import tokens

def p_expression_plus(p):
    'expression : expression PLUS term'
    p[0] = p[1] + p[3]

def p_expression_minus(p):
    'expression : expression MINUS term'
    p[0] = p[1] - p[3]

def p_expression_term(p):
    'expression : term'
    p[0] = p[1]

def p_term_times(p):
    'term : term TIMES factor'
    p[0] = p[1] * p[3]

def p_term_div(p):
    'term : term DIVIDE factor'
    p[0] = p[1] / p[3]

def p_term_factor(p):
    'term : factor'
    p[0] = p[1]

def p_factor_num(p):
    'factor : NUMBER'
    p[0] = p[1]

def p_factor_expr(p):
    'factor : LPAREN expression RPAREN'
    p[0] = p[2]

# Error rule for syntax errors
def p_error(p):
    print("Syntax error in input!")

# Build the parser
parser = yacc.yacc()

while True:
   try:
       s = raw_input('calc > ')
   except EOFError:
       break
   if not s: continue
   result = parser.parse(s)
   print(result)

고장

  • 각 문법 규칙은 해당 함수에 대한 docstring이 적절한 문맥 - 자유 문법 명세를 포함하는 함수에 의해 정의된다. 함수 본문을 구성하는 명령문은 규칙의 의미 론적 작업을 구현합니다. 각 함수는 해당 규칙에서 각 문법 기호의 값을 포함하는 시퀀스 인 단일 인수 p를 허용합니다. p[i] 의 값은 다음과 같이 문법 기호에 매핑됩니다.

      def p_expression_plus(p):
          'expression : expression PLUS term'
          #   ^            ^        ^    ^
          #  p[0]         p[1]     p[2] p[3]
    
          p[0] = p[1] + p[3]
    
  • 토큰의 경우 해당 p[i] 의 "값"은 렉서 모듈에 할당 된 p.value 속성과 동일합니다. 그래서 PLUS+ 값을 갖습니다.

  • 비 터미널의 경우, 값은 p[0] 있는 값에 의해 결정됩니다. 아무 것도 배치되지 않으면 값은 없음입니다. 또한, p[-1] 과 동일하지 p[3] 이후, p (a 단순리스트 아니다 p[-1] 매립 작업을 지정할 수있다 (생략)이 여기에 설명).

이 함수는 p_ 가 앞에 오는 한 어떤 이름이라도 가질 수 있습니다.

  • p_error(p) 규칙은 구문 오류를 catch하도록 정의됩니다 (yacc / bison의 yyerror 와 동일).

  • 여러 문법 규칙을 하나의 함수로 결합 할 수 있습니다. 프로덕션이 비슷한 구조를 갖는다면 좋은 아이디어입니다.

      def p_binary_operators(p):
          '''expression : expression PLUS term
                        | expression MINUS term
             term       : term TIMES factor
                        | term DIVIDE factor'''
          if p[2] == '+':
              p[0] = p[1] + p[3]
          elif p[2] == '-':
              p[0] = p[1] - p[3]
          elif p[2] == '*':
              p[0] = p[1] * p[3]
          elif p[2] == '/':
              p[0] = p[1] / p[3] 
    
  • 토큰 대신 문자 리터럴을 사용할 수 있습니다.

      def p_binary_operators(p):
          '''expression : expression '+' term
                        | expression '-' term
             term       : term '*' factor
                        | term '/' factor'''
          if p[2] == '+':
              p[0] = p[1] + p[3]
          elif p[2] == '-':
              p[0] = p[1] - p[3]
          elif p[2] == '*':
              p[0] = p[1] * p[3]
          elif p[2] == '/':
              p[0] = p[1] / p[3]
    

    물론, 리터럴은 렉서 모듈에서 지정되어야합니다.

  • 빈 프로덕션은 '''symbol : ''' 형식을 가지고 있습니다 '''symbol : '''

  • 시작 심볼을 명시 적으로 설정하려면 start = 'foo' . 여기서 foo 는 일부 비 터미널입니다.

  • 우선 순위 및 연관성 설정은 우선 순위 변수를 사용하여 수행 할 수 있습니다.

      precedence = (
          ('nonassoc', 'LESSTHAN', 'GREATERTHAN'),  # Nonassociative operators
          ('left', 'PLUS', 'MINUS'),
          ('left', 'TIMES', 'DIVIDE'),
          ('right', 'UMINUS'),            # Unary minus operator
      )
    

    토큰은 우선 순위가 낮거나 높은 순서로 정렬됩니다. nonassoc 은 해당 토큰이 연결되지 않음을 의미합니다. 즉 a < b < c 와 같은 것은 불법이며 a < b 는 여전히 합법적입니다.

  • parser.out 은 yacc 프로그램이 처음 실행될 때 작성되는 디버깅 파일입니다. 시프트 / 감소 충돌이 발생할 때마다 파서가 항상 시프트됩니다.



Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow