软件工程结对项目-小学四则运算题目生成与判题程序

软件工程结对项目

项目参与成员

计算机科学与技术3班 王坤平 3123004758

计算机科学与技术3班 曾梓垚 3123004764

这个作业属于哪个课程 https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class34Grade23ComputerScience/homework/13477
这个作业目标 实现一个自动生成小学四则运算题目的命令行程序

GitHub链接:(https://github.com/wang-kaopu/demo-calculator)

PSP表格相关记录

PSP2 1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 30
Estimate 估计这个任务需要多少时间 615 605
Development 开发 15 15
Analysis 需求分析(包括学习新技术) 60 90
Design Spec 生成设计文档 20 10
Design Review 设计复审 30 15
Coding Standard 代码规范(为目前的开发制定合适的规范) 30 30
Design 具体设计 60 60
Coding 具体编码 240 240
Code Review 代码复审 10 10
Test 测试(自我测试,修改代码,提交修改) 30 40
Reporting 报告 15 15
Test Report 测试报告 15 15
Size Measurement 计算工作量 30 15
Postmortem & Process Improvement Plan 事后总结,并提出过程改进计划 30 20

模块接口的设计与实现过程

总体说明

  • 目标:生成小学四则运算题目、解析与计算表达式、并对用户答案进行判题与评分。各模块职责清晰,尽量使用 Fraction 类型保证精度。
  • 公共约定:
    • 表达式树使用 tuple 结构 (op, left, right) 或 Fraction/int 作为叶子。
    • 所有 I/O 操作(读写文件)封装成可能抛出 FileOperationError 的高层 API。
    • 解析/计算失败使用领域异常:ExpressionParseError、RuleViolationError、ParameterValidationError 等。

errors.py — 异常模块

  • 目的:定义域内自定义异常类型,统一错误处理接口。
  • 公共类型(接口):
    • CalculatorError(Exception) — 基类。
    • RuleViolationError(CalculatorError) — 规则校验失败(如生成题目时出现负数、不合法分数等)。
    • ExpressionParseError(CalculatorError) — 解析表达式失败,构造器允许传入 message 与 text(原始文本)。
    • FileOperationError(CalculatorError) — 封装文件 I/O 错误,构造器接受 message 和可选 path 字段。
    • ParameterValidationError(CalculatorError) — 输入参数验证失败。

utils.py — 工具库

  • 主要职责:
    • 格式化与解析分数(format_fraction, parse_fraction)。
    • 将表达式树正规化用于去重(normalize_expr)。
    • 表达式词法与语法解析:tokenize、parse_expression(返回表达式树)。
    • 基础分数算术封装(fraction_add/sub/mul/div)与便利函数 is_proper_fraction。
  • 主要函数摘要:
    • format_fraction(frac: Fraction|int) -> str
      • 描述:把 Fraction 格式化为字符串,真分数显示为 "a'b/c" 或 "num/den" 或整数。
      • 错误模式:接受可转为 Fraction 的对象;不会抛错,除非传入非法类型(会在调用处报错)。
    • parse_fraction(s: str) -> Fraction
      • 描述:解析字符串到 Fraction,支持 "2'3/5"、"3/5"、"2"。
      • 错误模式:无法解析时抛 ExpressionParseError(含原文本)。
    • tokenize(expr: str) -> iterator[(kind, token)]
      • 描述:词法化;在遇到无法识别字符时抛 ExpressionParseError,提供位置信息。
    • parse_expression(s: str) -> expr_tree
      • 描述:基于递归下降解析实现,返回表达式树(tuple 或 Fraction/int)。
      • 错误模式:遇到语法错误抛 ExpressionParseError;接口保证在输入合法时返回可交给 eval_expr 的树。
    • normalize_expr(expr) -> str
      • 描述:把表达式树归一化为字符串键,支持 + 和 × 的交换律(将子项排序)。
      • 用途:用于题目去重集合的判定。

generator.py— 题目生成模块

  • 主要职责:
    • 随机生成题目表达式树(random_expr、random_number)。
    • 计算表达式值(eval_expr)。
    • 将表达式转换为字符串(expr_to_str)。
    • 批量生成并写入文件(generate(n, r) -> writes Exercises.txt & Answers.txt)。
  • 接口契约(函数签名):
    • random_number(r: int) -> int|Fraction
      • 行为:以50%概率返回自然数或真分数,范围基于 r。
      • 错误模式:假定 r 为 int 且 r > 1。
    • random_expr(r: int, max_ops: int=3) -> expr_tree
      • 描述:递归生成表达式树,max_ops 控制运算符数量。
    • eval_expr(expr) -> Fraction
      • 描述:递归计算表达式树的 Fraction 结果;在非法数学情况时抛 ValueError(负数、除以0、非真分数结果等)。
    • expr_to_str(expr) -> str
      • 描述:把表达式树转换为文本形式(带括号),叶子用 format_fraction 格式化。
    • generate(n: int, r: int) -> None
      • 描述:生成 n 道题并写入 Exercises.txt 与 Answers.txt,过程进行参数验证、去重(normalize_expr)、规则校验(捕获 RuleViolationError)及 I/O 错误封装为 FileOperationError。
      • 输出/副作用:在控制台打印生成数量;在异常情况下抛出 FileOperationError 或 ParameterValidationError。
  • 边界情况与错误模式:
    • 参数验证:n 必须为正 int,r 必须大于 1(否则抛 ParameterValidationError)。
    • 生成失败:在多次尝试后仍未生成足够题目,函数当前只是结束并写入已生成的题目;可增强以在不足时抛自定义异常或返回状态。
    • 数学规则:eval_expr 对减法、除法结果做严格检查(例如不允许负数和非真分数),并抛 ValueError;这被 generator 捕获并跳过该表达式。

grader.py — 判题模块

  • 主要职责:
    • 读取题目文件与用户答案文件,解析题目文本为表达式树,计算标准答案,比较用户答案并把统计写回 [Grade.txt](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-browser/workbench/workbench.html)。
  • 公共接口:
    • grade(exercise_file: str, answer_file: str) -> None
      • 输入:exercise_file(路径),answer_file(路径)
      • 行为:读取两文件、逐行解析表达式、计算标准值、解析用户答案(parse_fraction),比对并分类到 correct/wrong、最后写 [Grade.txt](vscode-file://vscode-app/d:/Microsoft VS Code/resources/app/out/vs/code/electron-browser/workbench/workbench.html)。
      • 错误模式:文件读写失败抛 FileOperationError;解析失败会将该题计为 wrong,并在日志中记录 debug;在极端未处理异常时捕获并记录,继续判题以保证输出。
  • 边界情况:
    • 文件行数不匹配:如果 user_answers 更短,后续题目不会被判分(未处理)。
    • 解析差异:若表达式解析成功但 eval_expr 抛异常,当前会被 broad except 捕获并计为 wrong(并输出到控制台)。

数据形状与示例

  • 表达式树示例:

    • 5 -> int or Fraction(5,1)
    • ( '+', 1, 2 ) -> 表示 1 + 2
    • ( '×', ( '+', 1, 2 ), 3 ) -> (1+2)×3
  • normalize_expr 返回例如 "+(1/2,3/4)" 用于集合去重。

  • generate 输出文件格式:

    • Exercises.txt的行: "1/2 + 3 ="
    • Answers.txt 的行: "7/2" 或 "3'1/2"(由 format_fraction 产生)
  • Grade.txt

    格式:

    • Correct: (list)
    • Wrong: (list)

错误与异常模型(总结)

  • 低层(解析/数学):
    • ExpressionParseError(解析问题)
    • ValueError(当前用于数学约束,如负数、除零)
  • 中层(生成/判题):
    • RuleViolationError(生成时违反表达式规则)
    • ParameterValidationError(非法参数)
  • 高层(I/O):
    • FileOperationError(读写失败,包含 path)

mermaid-diagram-2025-10-19-221301

代码分析及思路说明

分数封装 FractionNumber

  • 职责
    • 封装分数运算,提供稳定的数值表示与格式化;保持分数为最简形式并确保分母为正;支持带分数的输入/输出格式(例如 2'1/3)。
  • 实现要点
    • 使用 Python 的 Fraction 作为内部表示;在构造或任何运算后调用 Fraction 的 normalize(或自动约分)以保证最简形式。
    • 在字符串解析中支持多种格式:整数("2")、真分数("3/4")、假分数或带分数("2'1/3")。
    • format 方法输出规范化形式(整数、真分数或带分数),并在必要时把负号放置在整数或分子前保持一致。
  • 边界与错误处理
    • 对非法字符串或零分母抛出专门的解析异常(ExpressionParseError 或 ParseError)。
    • 在除法运算中捕获除以0并抛出 ZeroDivisionError 或自定义 MathError。

表达式树 BinaryOpNode(结构、递归求值与约束)

  • 职责
    • 使用二叉树节点表示带操作符的表达式,叶子为 FractionNumber 或整数;支持递归求值、字符串化与结构化哈希以用于去重。
  • 实现要点
    • 节点结构含 (op, left, right),left/right 可再次为节点或叶子。
    • eval 方法采用递归实现:先计算左右子树,再按 op 执行 Fraction 运算。
    • 在 eval 过程中进行题目约束检查(例如不允许负数结果、除法结果必须是真分数、不能除以0),违反约束时抛出 RuleViolationError。
  • 去重支持
    • 实现 get_structure_hash,针对交换律算子(+、×)对左右子树哈希进行排序,从而生成不受左右顺序影响的标准化哈希字符串。
  • 边界与错误处理
    • 对非法节点结构、缺失子节点或未知 op 抛适当的异常。

题目生成算法 generate_expression(递归构建与控制)

  • 职责
    • 随机且受控地构建表达式树以生成合格的题目,控制运算符数量、数值范围与规则约束。
  • 实现要点
    • 递归策略:根据 max_ops 决定当前是否生成叶子或再分为左右子树并选择运算符。
    • 随机性引入:随机选取运算符、数值(通过 generate_number),并对加/乘使用交换律时随机划分子运算数目以生成多样结构。
    • 限制与重试:对每次生成执行 eval 校验,若违反规则(如负数、非真分数、除0 等)则丢弃并重试;
    • 除法的特殊处理:生成除法时,优先保证被除数 / 除数 导出为真分数并且分母不为0(可通过构造分子为被除数*除数形式或缩放策略)。

去重哈希 get_structure_hash(等价归一化)

  • 职责
    • 为表达式树生成唯一且稳定的结构哈希,用于判断题目等价性(支持 +、× 的交换律与结合律简化处理)。
  • 实现要点
    • 递归序列化:对每个节点生成 key = f"{op}({left_key},{right_key})";对于交换律算子先对子 key 做排序再拼接,保证左右互换不影响 key。
  • 性能与存储
    • 使用集合(hash set)保存已出现的结构 key,插入与查找为 O(1)。

数字生成策略 generate_number(随机分布与约束)

  • 职责
    • 按设定概率策略在给定范围内生成整数、真分数、假分数或带分数。
  • 实现要点
    • 概率分配:示例为 20% 整数、20% 真分数、20% 任意分数、20% 带分数、20% 随机;实现中通过 random.random() 与区间判断控制分支。
    • 数值范围:所有生成数值保证在 [0, r)(或根据需求允许更大),并在生成后化为 Fraction 做进一步验证(如真分数检测)。
    • 约束:避免分母为0,优先生成易于计算的约数以避免复杂除法(或在除法场景下做后处理以保证整除或真分数)。

答案批改 compare_answers(解析、计算与精确比较)

  • 职责
    • 解析题目文本、计算标准答案、解析用户答案为 Fraction,并进行精确比较(数值而非字符串),最后输出详细批改报告或写入 牛魔Grade.txt。
  • 实现要点
    • 题目解析:清理题目行(去掉末尾 '=' 等),使用 parse_expression 解析为表达式树,再用 eval 计算 Fraction 标准答案。
    • 用户答案处理:parse_fraction 将各种答案格式解析为 Fraction;对解析失败则将该题记为错误并记录详细原因。
    • 比较逻辑:直接比较 Fraction 对象(相等性比较),而非字符串;记录 correct/wrong 的题号,写出统计报告。
    • 容错:遇到解析错误或计算异常不会中止整个批改流程,而是将该题计为 wrong 并继续下一题。

附:整体算法复杂度与效率说明

  • 生成端的主要成本在表达式构建与去重检查。每次生成的去重检查为 O(1)(哈希集合查重),但 normalize/结构哈希为 O(k)(k 为节点数)。总体上,生成 n 道题的时间大致为 O(n * avg_tree_size)。
  • parse_expression 与 eval_expr 递归复杂度为 O(k)(节点数)。
  • 内存使用:存储已生成题目的结构哈希集合占用 O(n * avg_key_size),可通过压缩或对 key 做轻量哈希减少内存。

模块接口的性能改进

image-20251021142442889

运行结果展示

  1. 生成题目

ea32866a-ff15-4ffb-b1e5-460b8481e7a5

使用方法:

生成题目:
python Myapp.py -n 10 -r 10
  1. 批改题目
判题:
python Myapp.py -e Exercises.txt -a Answers.txt

image-20251021142938579

image-20251021143452527

测试运行展示

生成题目:
python Myapp.py -n 10 -r 10

image-20251021143452527

Exercise.txt内容如下:

image-20251021143724476

Answers.txt内容如下:

image-20251021143828561

经测试,答案正确且运行结果符合预期

image-20251021143925212

image-20251021143942964

模块部分异常处理说明

异常处理一:规则检查处理

目标:检查数学运算中不符合题目规则要求的情况(如除以零、减法产生负值、除法结果不是真分数等)。

对应代码片段:

def __truediv__(self, other: 'FractionNumber') -> 'FractionNumber': # 除零异常检查
  if other.numerator == 0: # 检查是否存在除数为零的情况
    raise ZeroDivisionError("Cannot divide by zero.")
  new_num = self.numerator * other.denominator
  new_den = self.denominator * other.numerator
  return FractionNumber(new_num, new_den)

def __sub__(self, other: 'FractionNumber') -> 'FractionNumber': # 减法结果负值异常检查
  new_num = self.numerator * other.denominator - other.numerator * self.denominator
  new_den = self.denominator * other.denominator
  return FractionNumber(new_num, new_den)

elif self.op == '-': # 检查是否存在e1 < e2的情况
  if l.value() < r.value():
    raise ValueError("Subtraction would result in negative.")
  return l - r

def is_proper_fraction(fraction: FractionNumber) -> bool: # 真分数约束检查
  return abs(fraction.numerator) < fraction.denominator and fraction.denominator > 1

if op == '/': # 在除法运算后的验证
  res = l / r # 严格检查除法结果是否为真分数
  if not is_proper_fraction(res):
    raise ValueError("Division result must be a proper fraction...")

异常处理二:表达式解析异常处理

目标:处理用户输入表达式格式错误或求值错误(语法错误、非法字符、运算异常等)。

对应代码片段:

def evaluate_expression(expression: str) -> FractionNumber:
  expr_processed = preprocess_expression(expression)
  try:
    result = eval(expr_processed)
  except Exception as e:
    raise ValueError(f"表达式求值失败:{e}")
  if isinstance(result, Fraction): # 类型检查
    return FractionNumber(int(result.numerator), int(result.denominator))
  elif isinstance(result, int):
    return FractionNumber(result, 1)
  else:
    raise ValueError(f"无法识别的计算结果类型:{type(result)}")

异常处理三:文件操作异常处理

目标:确保文件读写操作的可靠性并在异常时给出明确提示或回退。

对应代码片段:

def compare_answers(exercise_file: str, answer_file: str):
  try:
    with open(exercise_file, 'r', encoding='utf-8') as f:
      exercises = f.readlines()
    with open(answer_file, 'r', encoding='utf-8') as f:
      answers = f.readlines() 
    if len(exercises) != len(answers):
      print(f"❌ 错误:题目数量({len(exercises)}) 与答案数量({len(answers)}) 不相等。")
      return
  except Exception as e:
    print(f"批改时发生错误:{e}")

异常处理四:题目生成重试机制

目标:处理生成过程中的临时错误和不满足规则的情况,确保最终输出满足约束或在失败时以明确方式终止。

对应代码片段:

def generate_expression(r: int, max_ops: int = MAX_OPERATORS) -> Tuple[str, FractionNumber, str]:
  num_tries = 0
  max_tries = 1000
  while num_tries < max_tries:
    num_tries += 1
    try: # 生成逻辑
      return expr_str, val, structure_hash
    except Exception as e:
      continue  # 静默重试
  raise RuntimeError("Failed to generate valid expression after many tries.")

异常处理五:参数验证异常

目标:确保程序入口输入参数合法,避免下游函数收到非法参数导致难以排查的错误。

对应代码片段:

def main():  # 参数解析
  if r < 1:
    print("❌ 错误:-r 后的数值范围必须 >= 1,例如:-r 10")
    return
  except ValueError:
    print("❌ 错误:-n 后必须跟一个整数,表示题目数量。例如:-n 10")
    return

项目总结

​ 通过此次结对编程项目,我们成功实现了需求规格中的所有功能,并在合作过程中获得了显著的成长与收获。

​ 在项目成果方面,结对编程模式使我们的代码质量得到了明显提升。通过实时审查和讨论,我们有效减少了因粗心导致的低级错误。在分数运算和表达式生成等复杂功能的实现过程中,持续的头脑风暴和即时反馈帮助我们攻克了多个技术难点。

​ 在个人成长与团队协作方面,这次合作为我们带来了宝贵的相互学习机会。我们一人在编码实现和调试方面展现出了出色的能力,熟练使用调试工具进行性能分析,快速定位问题根源;一人展现了优秀的架构思维,能够从整体设计角度避免陷入细节陷阱,同时在调试复杂逻辑时表现出极大的耐心和细致。这种优势互补的合作模式,特别是在代码规范和模块化设计方面的深入交流,为我们后续开发其他项目奠定了良好基础。

​ 在反思与改进方面,我们也意识到在逻辑严谨性上仍有提升空间,代码问题的排查效率有待进一步提高。建议在未来的项目中可以更系统地使用测试驱动开发,并建立更完善的调试流程。

​ 总体而言,这次结对编程项目取得了圆满成功,所有输出均符合预期结果。我们不仅高质量完成了项目要求,更重要的是建立了一套高效协作的模式,这种经验将对未来的学习和工作产生持续积极的影响。

posted @ 2025-10-21 15:09  naswyne  阅读(21)  评论(0)    收藏  举报