Python简易计算器怎么支持连续运算和括号优先级
许多初学者在编写 Python 计算器时,通常从两个数字的加减乘除开始,比如通过 input() 接收两个数值和一个符号完成一次计算。然而,一旦面对如 3 + 5 * 2 或 (2 + 3) * (8 - 4 / 2) 这类包含多步连续运算、先乘除后加减以及括号任意嵌套的需求时,原有的线性结构就会彻底失效。支持连续运算与括号优先级的核心路径,在于将平铺的字符流转化为有序的计算调度,本文将通过经典的“双栈算符优先法”带领读者实现一个无须依赖外部库且逻辑严谨的完整解析器。
一、简易计算器的瓶颈:为什么线性累加与正则替换行不通?
在尝试支持连续计算时,常见的一种直觉方案是“从左到右依次累加”。例如对表达式 2 + 3 * 4,若读取 2 + 3 算得 5,再用 5 * 4 算得 20,这显然违背了四则运算中乘除优先于加减的基础法则。正确的运算顺序必须先结合 3 * 4 = 12,再执行 2 + 12 = 14。
另一种看似合理的想法是使用正则表达式:“每次找到最内层的括号算出来替换回去,再用正则找到乘除算出来替换回去”。这种基于字符串模式匹配的方式在极简单场景下勉强可行,但在面对深层嵌套括号如 ((2 + 3) * (4 + (5 - 1))) 或相邻同级结合如 8 / 2 * 3 时,处理边界规则会迅速膨胀,极易陷入漏匹配或无限循环的泥潭。
四则运算的本质不是纯粹的一维流式处理,而是具有层级和延迟计算特性的树状逻辑。计算机科学中解决此类问题的经典工具是**栈(Stack)**。通过建立暂存未结算数据的结构,我们才能在遇到高优先级符号或括号时暂缓前置计算,并在适当时机回溯求解。
二、核心机制:双栈算符优先法原理
虽然直接调用 Python 的 eval() 可以瞬间求值,但 eval 具有任意代码执行的安全风险,且无助于理解底层计算过程。工业级和教学中通用且易于上手的方案是 Dijkstra 提出的算符优先算法(双栈法)。
该方法维护两个核心栈容器:
- 操作数栈(num_stack):按顺序存放待计算的数值(如整数、浮点数以及子步骤的计算结果)。
- 运算符栈(op_stack):按顺序存放运算符(
+、-、*、/)和括号标记((、))。
算法从左向右逐个扫描表达式中的每个符号(Token),并按照如下确定性规则驱动运算:
- 遇到数字:直接压入操作数栈。
- 遇到左括号
(:将其作为独立的隔离标志压入运算符栈。左括号后续的所有运算将被看作一个局部的子环境,直到遇到对应右括号。 - 遇到右括号
):表明当前子表达式结束,反复从运算符栈弹出符号并配合操作数栈计算,直到栈顶遇到左括号(为止,并将该左括号弹出丢弃。 - 遇到加减乘除运算符:比较当前运算符与运算符栈顶符号的优先级(预先定义
*、/优先级为 2,+、-优先级为 1,栈内的(为 0)。只要栈顶符号的优先级大于等于当前符号,说明栈顶的运算已经可以执行,立即弹出栈顶运算符并执行一次计算;重复此检查直至条件不满足,然后将当前运算符压入栈。 - 扫描完毕:表达式读取完成后,若运算符栈中仍有残留符号,依次弹出并执行计算,最终操作数栈仅剩的一个数值即为最终答案。
三、分步实现:从词法切分到完整计算器
将算法落地为代码,需要完成三个关键环节:表达式切分(Tokenize)、单步运算执行器以及主循环调度。
步骤一:提取与规范化 Token
用户输入的字符串往往包含空格,且数字可能是多位数(如 123)或包含小数。此外,减号 - 可能是双目减法运算符,也可能是单目负号(例如 -5 或 3 * (-2))。为了简化核心逻辑,我们先用一个扫描器将字符串拆解为合法的语义单元:
def tokenize(expr: str):
tokens = []
i = 0
n = len(expr)
while i < n:
char = expr[i]
if char.isspace():
i += 1
continue
if char in "+-*/()":
# 判断减号是否为一元负号:如果减号出现在开头,或紧跟在左括号/运算符之后
if char == '-':
prev = tokens[-1] if tokens else None
if prev is None or prev in "+-*/(":
# 作为负数的前缀处理
i += 1
start = i
while i < n and (expr[i].isdigit() or expr[i] == '.'):
i += 1
num_str = '-' + expr[start:i]
tokens.append(float(num_str) if '.' in num_str else int(num_str))
continue
tokens.append(char)
i += 1
elif char.isdigit() or char == '.':
start = i
while i < n and (expr[i].isdigit() or expr[i] == '.'):
i += 1
val_str = expr[start:i]
tokens.append(float(val_str) if '.' in val_str else int(val_str))
else:
raise ValueError(f"检测到非法字符: {char}")
return tokens
步骤二:单步运算与优先级映射
定义四则运算的单步计算函数,以及各运算符的优先级映射字典。注意,在双栈中弹出两个操作数时,先弹出的操作数实际位于运算符的右侧,必须注意减法和除法的顺序:
PRECEDENCE = {
'+': 1,
'-': 1,
'*': 2,
'/': 2
}
def apply_op(num_stack, op):
if len(num_stack) < 2:
raise ValueError("缺少操作数,表达式语法错误")
right = num_stack.pop()
left = num_stack.pop()
if op == '+':
num_stack.append(left + right)
elif op == '-':
num_stack.append(left - right)
elif op == '*':
num_stack.append(left * right)
elif op == '/':
if right == 0:
raise ZeroDivisionError("除数不能为0")
num_stack.append(left / right)
步骤三:核心计算器类完整代码
整合 Tokenizer 与双栈调度逻辑,编写一个完整可调用的 Calculator 类。该实现不依赖第三方依赖,直接在标准 Python 环境中即可执行:
class Calculator:
PRECEDENCE = {'+': 1, '-': 1, '*': 2, '/': 2}
@staticmethod
def calculate(expression: str):
# 1. 词法切分
tokens = Calculator._tokenize(expression)
if not tokens:
raise ValueError("表达式不能为空")
num_stack = []
op_stack = []
for token in tokens:
if isinstance(token, (int, float)):
num_stack.append(token)
elif token == '(':
op_stack.append(token)
elif token == ')':
# 遇到右括号,清算直到匹配左括号
while op_stack and op_stack[-1] != '(':
Calculator._apply_op(num_stack, op_stack.pop())
if not op_stack:
raise ValueError("括号不匹配:缺少对应左括号")
op_stack.pop() # 弹出并丢弃 '('
elif token in Calculator.PRECEDENCE:
curr_prec = Calculator.PRECEDENCE[token]
# 栈顶符号优先级更高或同级时,先计算栈顶
while (op_stack and op_stack[-1] != '(' and
Calculator.PRECEDENCE.get(op_stack[-1], 0) >= curr_prec):
Calculator._apply_op(num_stack, op_stack.pop())
op_stack.append(token)
else:
raise ValueError(f"未知符号: {token}")
# 2. 收敛清空剩余符号
while op_stack:
op = op_stack.pop()
if op == '(':
raise ValueError("括号不匹配:缺少对应右括号")
Calculator._apply_op(num_stack, op)
if len(num_stack) != 1:
raise ValueError("表达式格式错误,未能收敛为单一结果")
return num_stack[0]
@staticmethod
def _apply_op(num_stack, op):
if len(num_stack) < 2:
raise ValueError("语法错误:缺少足够的数字")
b = num_stack.pop()
a = num_stack.pop()
if op == '+':
num_stack.append(a + b)
elif op == '-':
num_stack.append(a - b)
elif op == '*':
num_stack.append(a * b)
elif op == '/':
if b == 0:
raise ZeroDivisionError("数学错误:除数不能为0")
num_stack.append(a / b)
@staticmethod
def _tokenize(expr: str):
tokens = []
i = 0
n = len(expr)
while i < n:
c = expr[i]
if c.isspace():
i += 1
continue
if c in "+-*/()":
# 负号判断:表达式开头或前一个 Token 为左括号/运算符
if c == '-':
prev = tokens[-1] if tokens else None
if prev is None or prev in "+-*/(":
i += 1
start = i
while i < n and (expr[i].isdigit() or expr[i] == '.'):
i += 1
s = '-' + expr[start:i]
tokens.append(float(s) if '.' in s else int(s))
continue
tokens.append(c)
i += 1
elif c.isdigit() or c == '.':
start = i
while i < n and (expr[i].isdigit() or expr[i] == '.'):
i += 1
s = expr[start:i]
tokens.append(float(s) if '.' in s else int(s))
else:
raise ValueError(f"非法字符: {c}")
return tokens
四、运行验证与测试用例
为了验证计算器在多级连续运算、嵌套括号以及负数环境下的稳定性,我们运行以下测试用例:
if __name__ == "__main__":
test_cases = [
"1 + 2 * 3", # 基础乘法优先
"(1 + 2) * 3", # 基础括号提升优先级
"10 - 2 * 3 + 8 / 4", # 混合连续加减乘除
"((2 + 3) * (5 - 2)) / 3", # 双重嵌套括号
"3 + 5 * (2 - 8 / (1 + 3))", # 深层嵌套
"-5 + 3 * 2", # 开头负数处理
"10 + (-2 * 3)", # 括号内负数
]
for expr in test_cases:
res = Calculator.calculate(expr)
print(f"{expr:30} = {res}")
测试运行所得的示例输出如下(明确标为示例输出):
1 + 2 * 3 = 7
(1 + 2) * 3 = 9
10 - 2 * 3 + 8 / 4 = 6.0
((2 + 3) * (5 - 2)) / 3 = 5.0
3 + 5 * (2 - 8 / (1 + 3)) = 3.0
-5 + 3 * 2 = 1
10 + (-2 * 3) = 4
五、常见错误排查与健壮性防御
在简易计算器的扩展过程中,读者最容易在以下三个场景遇到报错:
- 括号不匹配报错(Unmatched Parenthesis):如果在处理右括号时栈中没有遇到左括号,说明右括号多余;若全篇扫描结束运算符栈内仍残留有
'(',说明左括号未闭合。必须主动抛出具体的括号不匹配异常,方便调用者定位。 - 除数为零(ZeroDivisionError):在执行
apply_op时必须在弹出右操作数后立即检查是否等于零,避免底层抛出未处理的系统级崩溃。 - 浮点精度误差:Python 的浮点运算基于 IEEE 754 标准,如
0.1 + 0.2会得到0.30000000000000004。如果业务场景需要高精度金融或科学计算,可将解析结果替换为 Python 内置的decimal.Decimal类型。
六、总结与技术延伸
通过双栈算符优先法,我们清晰地将“字符解析”、“优先级调度”和“实际执行”解耦开来。这种结构不仅完整解决了任意深度的括号嵌套和连加连乘计算,还为后续的功能扩充打下了坚实基础。如果读者希望在此基础上进一步升级,可以考虑以下延伸方向:
- 增加幂运算支持(
^或**):幂运算具有“右结合性”(如2 ^ 3 ^ 2 = 2 ^ 9 = 512),在优先级比较循环中,同级情况下不触发弹栈,而是直接压栈即可支持。 - 扩展内置数学函数:支持
sin()、cos()、sqrt()等函数,只需将函数名作为高优先级的一元算符入栈,遇到右括号结算时作为单目运算处理。 - 进阶至抽象语法树(AST):若需要对表达式进行变量代入求值(如
f(x) = 2x + 1)或求导,可将上述双栈法改造为构建表达式树节点,从而迈入完整编译器前端的领域。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









