Python简易计算器怎么支持连续运算和括号优先级

Chloe
Chloe
发布时间:2026-10-26 | 8人浏览过
原创

许多初学者在编写 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 时,处理边界规则会迅速膨胀,极易陷入漏匹配或无限循环的泥潭。

Python简易计算器怎么支持连续运算和括号优先级
图1:运算顺序与括号优先级的原理对比示意

四则运算的本质不是纯粹的一维流式处理,而是具有层级和延迟计算特性的树状逻辑。计算机科学中解决此类问题的经典工具是**栈(Stack)**。通过建立暂存未结算数据的结构,我们才能在遇到高优先级符号或括号时暂缓前置计算,并在适当时机回溯求解。

二、核心机制:双栈算符优先法原理

虽然直接调用 Python 的 eval() 可以瞬间求值,但 eval 具有任意代码执行的安全风险,且无助于理解底层计算过程。工业级和教学中通用且易于上手的方案是 Dijkstra 提出的算符优先算法(双栈法)。

该方法维护两个核心栈容器:

  • 操作数栈(num_stack):按顺序存放待计算的数值(如整数、浮点数以及子步骤的计算结果)。
  • 运算符栈(op_stack):按顺序存放运算符(+、-、*、/)和括号标记((、))。

算法从左向右逐个扫描表达式中的每个符号(Token),并按照如下确定性规则驱动运算:

  1. 遇到数字:直接压入操作数栈。
  2. 遇到左括号 (:将其作为独立的隔离标志压入运算符栈。左括号后续的所有运算将被看作一个局部的子环境,直到遇到对应右括号。
  3. 遇到右括号 ):表明当前子表达式结束,反复从运算符栈弹出符号并配合操作数栈计算,直到栈顶遇到左括号 ( 为止,并将该左括号弹出丢弃。
  4. 遇到加减乘除运算符:比较当前运算符与运算符栈顶符号的优先级(预先定义 *、/ 优先级为 2,+、- 优先级为 1,栈内的 ( 为 0)。只要栈顶符号的优先级大于等于当前符号,说明栈顶的运算已经可以执行,立即弹出栈顶运算符并执行一次计算;重复此检查直至条件不满足,然后将当前运算符压入栈。
  5. 扫描完毕:表达式读取完成后,若运算符栈中仍有残留符号,依次弹出并执行计算,最终操作数栈仅剩的一个数值即为最终答案。
Python简易计算器怎么支持连续运算和括号优先级
图2:双栈法处理各类 Token 时的分支逻辑与收敛路径

三、分步实现:从词法切分到完整计算器

将算法落地为代码,需要完成三个关键环节:表达式切分(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)
Python简易计算器怎么支持连续运算和括号优先级
图3:表达式在双栈中的数据流转与弹栈计算时序示意

步骤三:核心计算器类完整代码

整合 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 类型。
Python简易计算器怎么支持连续运算和括号优先级
图4:计算器模块分层设计与多重异常防御机制

六、总结与技术延伸

通过双栈算符优先法,我们清晰地将“字符解析”、“优先级调度”和“实际执行”解耦开来。这种结构不仅完整解决了任意深度的括号嵌套和连加连乘计算,还为后续的功能扩充打下了坚实基础。如果读者希望在此基础上进一步升级,可以考虑以下延伸方向:

  • 增加幂运算支持(^ 或 **):幂运算具有“右结合性”(如 2 ^ 3 ^ 2 = 2 ^ 9 = 512),在优先级比较循环中,同级情况下不触发弹栈,而是直接压栈即可支持。
  • 扩展内置数学函数:支持 sin()、cos()、sqrt() 等函数,只需将函数名作为高优先级的一元算符入栈,遇到右括号结算时作为单目运算处理。
  • 进阶至抽象语法树(AST):若需要对表达式进行变量代入求值(如 f(x) = 2x + 1)或求导,可将上述双栈法改造为构建表达式树节点,从而迈入完整编译器前端的领域。
相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表