デジタル忍者ブログ

デジタル忍者ブログ

2021/07/25

pythonでパーサを作る(#4:括弧つき四則演算)


今回は括弧付き四則演算のパーサを作ります。


この場合、カッコ内の式を先に処理する必要が出てきます。


BNFは次の通りです。


<数式> := <数式> + <項> | <数式> - <項> | <項>

<項> := <項> * <因子> | <項> / <因子> | <因子>

<因子> := <整数> | (<数式>)

<整数> := 0|1|2|3|4|5|6|7|8|9|<整数>


ここで因子の構文内に括弧付きの数式が定義されているため、


再帰関数等を使い括弧内の数式を処理してくことになります。




次のソースコードは数式の解析するためのクラスを定義し、


括弧が現れたときに、自身のクラスのインスタンスを生成してその続きから


対応する括弧の終わりまでを解析することで、再帰処理を解決しています。


ここでいう対応する括弧の終わりとは、


単純に末尾から近い括弧を示していることではありません。



例えば、次の式は注意が必要です。


1 + (2 + (3 + (4 + (5 + 6)) - 7) - 8) + 9


1つ目の'('に対応する括弧の終わりは確かに末尾から近い')'のですが、


2つ目の'('に対応する括弧の終わりは、数字7の後の')'であり、


必ずしも末尾から近い')'ではないことに注意してください。


こうした処理はスタックを利用して、カッコ内の数式を取り出して再帰処理する方法も取れます。


今回は')'が現れたときに、数式の読み取る位置のインデックス(idx)をあえてlen(入力した数式)にして、


強制的にその時点での値を返します。



動きを詳しく知りたい方は、


ソースコード上のコメントアウトを除いて実行してみると良いでしょう。


from collections import deque

class ExpressionParser():

  def __init__(self, idx = 0):
    self.idx = idx
    self.termList = deque([])
  
  def getIndex(self):
    return self.idx
  
  def calc(self, token):
#    print(self.termList, token)
    num1 = self.termList.pop()
    num2 = self.termList.pop()
    if token == '+':
      self.termList.append(num2 + num1)
    if token == '-':
      self.termList.append(num2 - num1)
    if token == '*':
      self.termList.append(num2 * num1)
    if token == '/':
      self.termList.append(num2 / num1)
    

  def getToken(self, expr):
    token = ''
    while True:
      if self.idx >= len(expr):
        break
      unit = expr[self.idx: self.idx + 1]
      if unit != ' ':
        if unit.isdigit():
          while unit.isdigit():
            token += unit
            self.idx += 1
            unit = expr[self.idx: self.idx + 1]
          self.idx -= 1
          break
        else:
          token = unit
          break
      self.idx += 1
#    print('get Token:',token)
    return token


  def parseExpression(self, expr):
    self.parseTerm(expr)
    token = self.getToken(expr)
    if len(token) == 0:
      return self.termList.pop()
#    print("parseExpression Token:", token)
    while token == '+' or token == '-':
      self.idx += 1
      self.parseTerm(expr)
      self.calc(token)
      token = self.getToken(expr)
    return self.termList.pop()
    
    
  def parseTerm(self, expr):
    self.parseFactor(expr)
    token = self.getToken(expr)
    if len(token) == 0:
      return
#    print("parseTerm Token:", token)
    while token == '*' or token == '/':
      self.idx += 1
      self.parseFactor(expr)
      self.calc(token)
      token = self.getToken(expr)


  def parseFactor(self, expr):
    token = self.getToken(expr)
#    print("parseFactor Token:", token)
    if token.isdigit():
      self.termList.append(int(token))
      self.idx += 1
    if token == '(':
      parser = ExpressionParser(self.idx + 1)
      token = parser.parseExpression(expr)
      self.idx = parser.getIndex() + 1
      self.termList.append(token)
    if token == ')':
      self.idx = len(expr)


if __name__ == '__main__':
  while True:
    print('Please input expression.(If finish this program, input "exit".):')
    expression = input()
    if expression == 'exit':
      break
    transaction = ExpressionParser()
    print("answer:", transaction.parseExpression(expression))
  

結果は次の通りです。

> python parser.py
Please input expression.(If finish this program, input "exit".):
4 * 3 + 2 * 1
answer: 14
Please input expression.(If finish this program, input "exit".):
3 * (2 / 2)
answer: 3.0
Please input expression.(If finish this program, input "exit".):
1 + (2 + (3 + (4 + (5 + 6)) - 7) - 8) + 9
answer: 15
Please input expression.(If finish this program, input "exit".):
1 * (2 * (3 * (4 * (5 + 6)) - 7) - 8) + 9
answer: 251
Please input expression.(If finish this program, input "exit".):
exit

> 

Comment Form

コメント内容(必須)

Comment

2022年11月27日15:14  test

不適切な内容が含まれている可能性があるため、削除されました。