结对编程练习 - 小学四则运算练习程序

合作同学学号: 2352233

算法设计思路

1. 核心需求与挑战

本项目的核心目标是构建一个交互式的小学四则运算练习工具。主要挑战包括:

  • 表达式生成: 如何程序化地生成结构有效、难度适中的随机四则运算表达式(包含 +, -, *, / 及整数);
  • 精确计算: 如何严格遵循数学运算优先级(先乘除后加减)并保证计算结果的绝对精确,特别是处理除法运算,避免浮点数精度问题;
  • 结果有效性约束: 如何确保生成的题目其计算结果满足特定约束(如大于 0 且小于 1000),并规避无效运算(如除以零);
  • 用户交互与健壮性: 如何设计清晰的交互流程,接收用户答案,准确判断对错,并优雅地处理无效输入或特殊指令 (skip, exit)。

2. 关键技术选型与实现策略

2.1 面向对象封装 (Expression 类)

设计决策: 采用面向对象方法,将与单个算术表达式相关的状态(操作数、运算符、表达式字符串、计算结果)和行为(生成、中缀转后缀、后缀求值)封装在 Expression 类中。

实现细节:

  • __init__:初始化运算符列表。
  • generate:负责随机生成数字和运算符,组装成中缀表达式字符串,并调用内部方法计算结果。包含关键的结果验证
  • infix_to_postfix:将中缀表达式转换为后缀表达式(RPN)。
  • evaluate_rpn:使用栈数据结构计算 RPN 表达式的值。

优势: 提高代码的模块化、可读性和可维护性。隐藏了表达式求值的复杂内部逻辑。

2.2 表达式生成与验证 (generate 方法)

随机性: 利用 random.randint 生成操作数,random.choice 选择运算符。通过 num_terms 参数控制表达式长度。

健壮性与结果约束:

  • 核心机制: 采用 while not positive_result 循环结构。
  • 异常处理: 在循环内部使用 try...except ZeroDivisionError 捕获并处理除零异常,若发生则放弃当前表达式并重新生成。
  • 结果验证: 在成功计算出 value 后,检查其是否满足 0 < value < 1000 的条件。只有满足所有条件(无异常、结果在有效范围)时,循环才会终止,确保生成的每个题目都是有效的。

2.3 表达式求值:中缀 -> 后缀 -> 求值

策略选择: 避免直接解析带优先级的中缀表达式的复杂性,采用业界成熟的两阶段方法:

  1. 中缀转后缀 (infix_to_postfix):
    利用操作符栈和输出队列,根据运算符的优先级 (precedence) 和结合性 (associativity) 将中缀表达式转换为等价的后缀表达式。 RPN 天然地消除了括号和优先级问题。
  2. 后缀表达式求值 (evaluate_rpn):
    使用一个操作数栈。遍历 RPN 序列,遇到数字压栈;遇到运算符则弹出所需数量的操作数进行计算,并将结果压回栈。最终栈顶元素即为表达式结果。

优势: 逻辑清晰,将 语法解析(优先级处理)与 计算执行 分离,易于实现和调试。

2.4 精度保证 (fractions.Fraction)

问题识别: 标准浮点数 (float) 进行除法运算可能产生精度损失,导致比较结果不可靠(例如 1/3)。

解决方案:evaluate_rpn 中计算以及在 exam 函数中处理用户输入和比较答案时,全面使用 Python 内置的 fractions.Fraction 类。

优势: Fraction 提供精确的有理数运算,彻底解决了浮点数精度问题,确保了计算和答案比对的绝对准确性。

2.5 测验流程控制与交互 (exam 函数)

主循环: 使用 for 循环控制总题目数量。

单题交互: 内部 while True 循环处理每一道题目的展示、用户输入获取、答案判断或命令处理。

输入健壮性:

  • 使用 input() 获取原始输入。
  • 优先检查是否为特殊命令 (skip, exit)。
  • 使用 try...except ValueError 捕获用户输入无法转换为 Fraction 的情况(如输入非数字、格式错误),提示用户重新输入,避免程序崩溃。

即时反馈与统计: 立即告知用户答案对错,并实时更新 answered, correct, wrong, skipped 计数器。

2.6 程序入口与总结 (main 函数)

职责: 调用 exam 函数启动测验,接收返回的统计数据,并在测验结束后格式化输出最终结果报告,包括正确率(注意处理 answered 为 0 的边界情况,避免除零错误)。

标准实践: 使用 if __name__ == "__main__": 保护主程序入口,确保 main() 只在脚本直接运行时执行。

3. 设计原则总结

正确性优先: 采用标准算法和 Fraction 确保计算逻辑和结果的精确无误。

健壮性设计: 通过异常处理和显式验证循环,处理潜在运行时错误(除零、无效输入)和业务逻辑约束(结果范围)。

模块化与封装: 利用类和函数划分功能,降低耦合度,提升代码可维护性。

用户体验: 提供清晰的交互提示、即时反馈和控制指令。

程序代码

main.py
import random
from fractions import Fraction


class Expression:
    def __init__(self):
        self.ops = ["+", "-", "*", "/"]
        self.expression = None
        self.value = None

    def generate(self, num_terms=3):
        if num_terms < 2:
            raise ValueError("Number of terms must be at least 2.")

        positive_result = False

        while not positive_result:
            try:
                # Generate random numbers
                numbers = [str(random.randint(1, 10)) for _ in range(num_terms)]

                # Generate random operators
                operators = [random.choice(self.ops) for _ in range(num_terms - 1)]

                # Build the infix expression string
                expression_list = []
                for i in range(num_terms - 1):
                    expression_list.append(numbers[i])
                    expression_list.append(operators[i])
                expression_list.append(numbers[-1])

                # Join the expression into a string
                self.expression = " ".join(expression_list)

                # Evaluate the expression using shunting yard algorithm and RPN evaluation
                rpn = self.infix_to_postfix(self.expression)
                self.value = self.evaluate_rpn(rpn)

                # Check if the result is greater than 0
                if 0 < self.value < 1000:
                    positive_result = True

            except ZeroDivisionError:
                continue

    def infix_to_postfix(self, expression):
        # Operator precedence and associativity
        precedence = {"+": 1, "-": 1, "*": 2, "/": 2}
        associativity = {"+": "L", "-": "L", "*": "L", "/": "L"}
        output_queue = []
        operator_stack = []
        tokens = expression.split()

        for token in tokens:
            if token.isdigit():
                output_queue.append(token)
            elif token in self.ops:
                while operator_stack:
                    top = operator_stack[-1]
                    if top in self.ops and (
                        (precedence[top] > precedence[token])
                        or (precedence[top] == precedence[token] and associativity[token] == "L")
                    ):
                        output_queue.append(operator_stack.pop())
                    else:
                        break
                operator_stack.append(token)
            else:
                # We do not have parentheses in the expression
                pass

        while operator_stack:
            output_queue.append(operator_stack.pop())

        return output_queue

    def evaluate_rpn(self, rpn):
        stack = []
        for token in rpn:
            if token.isdigit():
                stack.append(Fraction(int(token)))
            elif token in self.ops:
                y = stack.pop()
                x = stack.pop()
                result = None
                if token == "+":
                    result = x + y
                elif token == "-":
                    result = x - y
                elif token == "*":
                    result = x * y
                elif token == "/":
                    if y == 0:
                        raise ZeroDivisionError("Division by zero")
                    result = x / y
                stack.append(result)
            else:
                pass
        return stack[0]


def exam():
    answered = 0
    correct = 0
    wrong = 0
    skipped = 0

    for i in range(300):
        expr = Expression()
        expr.generate(random.randint(3, 6))
        if expr.expression is None:
            exit(1)
        print_expression = expr.expression.replace("/", "÷").replace("*", "×")
        print(f"{i:4}: {print_expression}", end="")
        while True:
            answer = input(" = ")
            if answer == "skip":
                print("Skipped, the answer is", expr.value)
                answered += 1
                skipped += 1
                break
            if answer == "exit":
                return answered, correct, wrong, skipped
            try:
                answer = Fraction(answer)
                if expr.value == answer:
                    print("Correct!")
                    answered += 1
                    correct += 1
                    break
                else:
                    print(f"Wrong! The correct answer is {expr.value}")
                    answered += 1
                    wrong += 1
                    break
            except ValueError:
                print("Invalid input. Please enter a valid fraction.")
                continue

    return answered, correct, wrong, skipped


def main():
    answered, correct, wrong, skipped = exam()
    print(f"\nAnswered: {answered}, Correct: {correct}, Wrong: {wrong}, Skipped: {skipped}")
    if answered > 0:
        print(f"Score: {correct / answered * 100:.2f}%")
    else:
        print("No questions answered.")


if __name__ == "__main__":
    main()

运行截图

img

体会与感悟

本次与同学(学号 2352233)通过结对编程方式共同开发小学四则运算练习程序的经历,是一次宝贵的学习与实践过程。

技术层面

  1. 算法与数据结构的应用: 实践中深刻体会到经典算法(如调度场算法用于中缀转后缀)和数据结构(如栈在 RPN 求值中的应用)的实用价值。它们为解决特定问题(如处理运算符优先级)提供了优雅且高效的范式。
  2. 精度问题的处理: 初步构思时可能忽略除法带来的精度陷阱。认识到 float 的局限性,并学习和应用 fractions.Fraction 来保证计算的绝对精确,是本次开发中的一个重要技术收获。这强调了在数值计算场景下对数据类型选择的审慎性。
  3. 健壮性设计的重要性: 编写“能运行”的代码相对容易,但编写“健壮”的代码则需要更多考量。通过 try-except 块处理潜在的 ZeroDivisionErrorValueError,以及设计 generate 方法中的结果验证循环,让我体会到主动防御式编程的必要性,确保程序在面对异常输入或边界情况时能稳定运行或按预期逻辑处理,而不是直接崩溃。
  4. 面向对象思维: 将表达式的属性和行为封装在 Expression 类中,使得主控流程 (exam 函数) 的逻辑更清晰,只需关注与用户的交互和 Expression 对象的调用,降低了心智负担。

协作与流程层面

  1. 结对编程的优势:
    • 即时反馈与代码审查: 两人同时思考和编码,可以立即发现对方逻辑上的疏漏或编码风格问题,有效提升了代码质量。
    • 知识共享与互补: 对于某些技术点或库函数,一方不熟悉时,另一方可以快速解释或演示,加速了学习过程。例如,在讨论如何处理精确计算时,共同研究并确定了使用 Fraction
    • 问题解决效率: 遇到 bug 或逻辑障碍时,两人讨论能更快地定位问题并找到解决方案,避免了单人长时间卡壳。
  2. 沟通的价值: 清晰、及时、有效的沟通是结对编程成功的基石。我们需要准确表达自己的想法,理解对方的意图,并在设计决策上达成一致。例如,共同确定 skip 功能的加入,以及如何处理用户输入异常的策略,都需要充分讨论。
  3. 规范与约定的作用: 虽然项目不大,但也体会到统一编码风格、明确函数职责等简单约定对协作顺畅度的提升。

总结

这次结对编程实践不仅锻炼了我的 Python 编程技能,特别是在算法应用、异常处理和数值精度控制方面,更让我切身体验了团队协作开发的模式。理解需求、设计健壮的解决方案、编写清晰的代码,并通过有效沟通与协作共同完成目标,这些都是软件开发中不可或缺的能力。我期待未来能将这些经验应用到更复杂的项目中。

posted @ 2025-04-17 15:18  Aaron212  阅读(28)  评论(0)    收藏  举报