首页 > 技术文章 > 用Python3实现表达式求值

maples7 2016-02-25 11:27 原文

一、题目描述

  请用 python3 编写一个计算器的控制台程序,支持加减乘除、乘方、括号、小数点,运算符优先级为括号>乘方>乘除>加减,同级别运算按照从左向右的顺序计算。

二、输入描述

  1. 数字包括"0123456789",小数点为".",运算符包括:加("+")、减("-")、乘("*")、除("/")、乘方("^",注:不是**!)、括号("()")
  2. 需要从命令行参数读入输入,例如提交文件为 main.py,可以用 python3 main.py "1+2-3+4" 的方式进行调用
  3. 输入需要支持空格,即 python3 main.py "1     +     2      -     3    +    4" 也需要程序能够正确给出结果
  4. 所有测试用例中参与运算的非零运算数的绝对值范围保证在 10^9-10^(-10) 之内, 应该输出运算结果时非零运算结果绝对值也保证在该范围内

三、输出描述

  1. 数字需要支持小数点,输出结果取10位有效数字,有效数字位数不足时不能补0
  2. 对于不在输入描述内的输入,输出INPUT ERROR
  3. 对于格式不合法(例如括号不匹配等)的输入,输出 FORMAT ERROR
  4. 对于不符合运算符接收的参数范围(例如除0等)的输入,输出VALUE ERROR
  5. 对于2、3、4的情况,输出即可,不能抛出异常
  6. 同时满足2、3、4中多个条件时,以序号小的为准

四、样例

输入: 1 + 2 - 3 + 4

输出: 4

输入: 1 + 2 - 3 + 1 / 3

输出: 0.3333333333

输入: 1 + + 2

输出: FORMAT ERROR

输入: 1 / 0

输出: VALUE ERROR

输入: a + 1

输出: INPUT ERROR

 

【注:此题为TsinghuaX:34100325X 《软件工程》 MOOC 课程 Spring, 2016 Chapter 1 Problem,此文发布时,这门课刚刚在 “学堂在线” 上开课两天】

 


 

用 Python3 实现,初看一下,首先想到的其实是一种“讨巧”(作弊 >_<)的方法(由于曾经网站被挂码的悲壮历史……),即通过 eval() 函数(这应该是黑客用得最多的一个函数了吧+_+)直接求解表达式,谁叫题目指定用 Python 这种方便的脚本语言呢~

大致代码区区几行:

1 from sys import argv
2 
3 if __name__ == "__main__":
4     exp = argv[1]
5     print(eval(exp))

即便是考虑到题干中的输出要求做异常处理(try...except)输出相应的错误信息,也就十行左右。

但稍深入就会发现,有些情形还是无法按要求处理的,比如样例 “1 + + 2”,用 eval() 会输出结果 “3” (+2 前的 “+” 被当作正号),而题目要求输出 “FORMAT ERROR”。

没办法,只能老老实实做苦力活儿了。

 

表达式求值其实是《数据结构》课程里一个基本且重要的问题之一,一般作为 “栈” 的应用来提出。

问题的关键就是需要按照人们通常理解的运算符的优先级来进行计算,而在计算过程中的临时结果则用 栈 来存储。

为此,我们可以首先构造一个 “表” 来存储当不同的运算符 “相遇” 时,它们谁更 “屌” 一些(优先级更高一些)。这样就可以告诉计算机,面对不同的情形,它接下来应该如何来处理。

其次,我们需要构造两个栈,一个运算符栈,一个运算数栈。

运算符栈是为了搞定当某个运算符优先级较低时,暂时先让它呆在栈的底部位置,待它可以 “重见天日” 的那一天(优先级相对较高时),再把它拿出来使用。正确计算完成后,此栈应为空。

运算数栈则是为了按合理的计算顺序存储运算中间结果。正确计算完成后,此栈应只剩下一个数,即为最后的结果。

 

完整的代码如下:

  1 # -*- coding: utf-8 -*-
  2 
  3 #################################
  4 # @Author:            Maples7
  5 # @LaunchTime:        2016/2/24 12:32:38
  6 # @FileName:        main
  7 # @Email:            maples7@163.com
  8 # @Function:
  9 #                   
 10 #       A Python Calculator for Operator +-*/()^
 11 #
 12 #################################
 13 
 14 from sys import argv
 15 from decimal import *
 16 
 17 def delBlank(str):
 18     """
 19     Delete all blanks in the str
 20     """
 21     ans = ""
 22     for e in str:
 23         if e != " ":
 24             ans += e
 25     return ans
 26 
 27 def precede(a, b):
 28     """
 29     Compare the prior of operator a and b
 30     """
 31     # the prior of operator
 32     prior = (
 33         #   '+'  '-'  '*'  '/'  '('  ')'  '^'  '#'
 34            ('>', '>', '<', '<', '<', '>', '<', '>'), # '+'
 35            ('>', '>', '<', '<', '<', '>', '<', '>'), # '-'
 36            ('>', '>', '>', '>', '<', '>', '<', '>'), # '*'
 37            ('>', '>', '>', '>', '<', '>', '<', '>'), # '/'
 38            ('<', '<', '<', '<', '<', '=', '<', ' '), # '('
 39            ('>', '>', '>', '>', ' ', '>', '>', '>'), # ')'
 40            ('>', '>', '>', '>', '<', '>', '>', '>'), # '^'
 41            ('<', '<', '<', '<', '<', ' ', '<', '=')  # '#'
 42         )
 43 
 44     # operator to index of prior[8][8]
 45     char2num = {
 46         '+': 0,
 47         '-': 1,
 48         '*': 2,
 49         '/': 3,
 50         '(': 4,
 51         ')': 5,
 52         '^': 6,
 53         '#': 7
 54         }
 55 
 56     return prior[char2num[a]][char2num[b]]
 57 
 58 def operate(a, b, operator):
 59     """
 60     Operate [a operator b]
 61     """
 62     if operator == '+':
 63         ans = a + b
 64     elif operator == '-':
 65         ans = a - b
 66     elif operator == '*':
 67         ans = a * b
 68     elif operator == '/':
 69         if b == 0:
 70             ans = "VALUE ERROR"
 71         else:
 72             ans = a / b
 73     elif operator == '^':
 74         if a == 0 and b == 0:
 75             ans = "VALUE ERROR"
 76         else:
 77             ans = a ** b
 78 
 79     return ans
 80 
 81 def calc(exp):
 82     """
 83     Calculate the ans of exp
 84     """
 85     exp += '#'
 86     operSet = "+-*/^()#"
 87     stackOfOperator, stackOfNum = ['#'], []
 88     pos, ans, index, length = 0, 0, 0, len(exp)
 89     while index < length:
 90         e = exp[index]
 91         if e in operSet:
 92             # calc according to the prior
 93             topOperator = stackOfOperator.pop()
 94             compare = precede(topOperator, e)
 95             if compare == '>':
 96                 try:
 97                     b = stackOfNum.pop()
 98                     a = stackOfNum.pop()
 99                 except:
100                     return "FORMAT ERROR"
101                 ans = operate(a, b, topOperator)
102                 if ans == "VALUE ERROR":
103                     return ans
104                 else:
105                     stackOfNum.append(ans)
106             elif compare == '<':
107                 stackOfOperator.append(topOperator)
108                 stackOfOperator.append(e)
109                 index += 1
110             elif compare == '=':
111                 index += 1
112             elif compare == ' ':
113                 return "FORMAT ERROR"
114         else:
115             # get the next num
116             pos = index
117             while not exp[index] in operSet:
118                 index += 1
119             temp = exp[pos:index]
120 
121             # delete all 0 of float in the end
122             last = index - 1
123             if '.' in temp:
124                 while exp[last] == '0':
125                     last -= 1
126                 temp = exp[pos:last + 1]
127 
128             try:
129                 temp = Decimal(temp)
130             except:
131                 return "INPUT ERROR"
132             stackOfNum.append(temp)
133 
134     if len(stackOfNum) == 1 and stackOfOperator == []:
135         return stackOfNum.pop()
136     else:
137         return "INPUT ERROR"
138 
139 if __name__ == "__main__":
140     # get the exp
141     exp = argv[1]
142 
143     # set the precision
144     getcontext().prec = 10
145 
146     # delete blanks
147     exp = delBlank(exp)
148 
149     # calc and print the ans
150     ans = calc(exp)
151     print(ans)

其中需要稍微注意的细节有:

1. 表达式处理前,前后都插入一个 '#' 作为一个特殊的运算符,这样做是为了方便统一处理,即不用再去特别判断表达式是否已经结束(从而引发一系列边界问题导致代码冗长复杂,这种处理也可称之为 “哨兵” 技巧)。如果最后两个运算符相遇则说明表达式处理完毕,这个运算符的优先级也是最低的(在 prior 表中也有体现)。

2. 输出要求比较复杂,抛去错误信息输出不说(只能具体情况具体分析),不能输出多余的0,折腾了一会儿最后发现用高精度的 Decimal 可以完美满足题目要求。

3. 由于不能输出多余的0,所以在带有小数部分的数字 “录入” 时(代码115-132行),就要把一些多余的0提前去掉(代码121-126行),比如 2.0 这样的情况。

推荐阅读