软件工程结对项目 小学四则运算题目生成与自动批改
| 项目 | 内容 |
|---|---|
| 所属课程 | 软件工程 |
| 作业要求 | 结对项目 |
| 项目成员 | 陈昱绰(3123004519)、陈鹏旭(3124004200) |
| 项目地址 | GitHub 仓库 |
| 程序下载 | 源码与 Windows 程序包 |
本项目用 Python 实现小学四则运算题目生成和自动批改。开发与复核使用了 AI 工具辅助,测试结果和性能数据均来自实际运行。
一、PSP 时间表
下表为工作量估算,单位是分钟。计划、开发和报告按各自子项汇总。
| PSP2.1 | 工作内容 | 预估耗时 | 参考耗时(估算) |
|---|---|---|---|
| Planning | 计划 | 10 | 10 |
| · Estimate | 估计任务所需时间 | 10 | 10 |
| Development | 开发 | 140 | 155 |
| · Analysis | 需求分析 | 25 | 30 |
| · Design Spec | 设计文档 | 10 | 10 |
| · Design Review | 设计复审 | 10 | 10 |
| · Coding Standard | 代码规范 | 5 | 5 |
| · Design | 具体设计 | 15 | 15 |
| · Coding | 编码与修改 | 25 | 30 |
| · Code Review | 代码复审 | 20 | 20 |
| · Test | 运行与测试 | 30 | 35 |
| Reporting | 报告 | 30 | 35 |
| · Test Report | 测试报告 | 15 | 20 |
| · Size Measurement | 计算工作量 | 5 | 5 |
| · Postmortem & Process Improvement Plan | 总结与改进计划 | 10 | 10 |
| 合计 | 180 | 200 |
二、需求理解
程序分为生成和批改两种模式。生成时,-n 指定题目数量,-r 指定操作数和分数分母的上限,不包含上限本身。题目写入 Exercises.txt,对应答案写入 Answers.txt。批改时,用 -e、-a 分别指定题目文件和学生作答文件,结果保存到 Grade.txt。
每道题最多 3 个运算符,计算过程中不能出现负数,除法结果必须是真分数。这里要检查每一步运算,不能只看最后的答案。
去重比较容易理解错。按题目规定,3+(2+1) 和 1+2+3 重复,1+2+3 与 3+2+1 不重复,所以不能直接按答案判断。
三、程序设计
程序按职责拆成四个部分:
| 文件 | 主要作用 |
|---|---|
Myapp.py |
读取参数,选择生成或批改模式,处理错误提示 |
quiz/model.py |
分数处理、表达式树、题面输出和表达式解析 |
quiz/generate.py |
随机生成合法题目并去重 |
quiz/files.py |
文件读写、题号检查和批改统计 |
表达式用二叉树保存,叶子是数,其他节点是运算符。生成时先确定运算符数量,再递归创建左右子树。
减法把较大的子表达式放在左边。除法把较小的正数放在左边,无法满足要求时改用其他运算。每生成一道题就检查是否重复,直到数量够用。
四、关键实现
1. 分数计算
分数统一使用 Python 标准库中的 Fraction,避免将分数转成小数后产生误差。
from fractions import Fraction
result = Fraction(1, 6) + Fraction(1, 8)
# 结果为 7/24
输出时把假分数转成带分数,如 19/8 写成 2'3/8。批改按数值比较,6/4 和 1'1/2 都可以判对。
2. 题目去重
去重的关键代码如下:
def key(self) -> tuple:
if not self.op:
return ("n", self.value.numerator, self.value.denominator)
left, right = self.left.key(), self.right.key()
if self.op in ("+", "*") and right < left:
left, right = right, left
return (self.op, left, right)
加法和乘法的左右键按固定顺序排列,减法和除法保持原顺序。这样得到的键既能处理交换顺序的情况,也保留了原来的树形结构。把键放入集合,就可以查重。
3. 括号与批改
括号需要保留原来的运算结构,尤其是同优先级的右子表达式。例如 1+(2+3) 的括号不能直接省掉。
批改时重新解析题目,再计算答案。解析器按加减、乘除、数字或括号分层处理,3/4 作为一个分数读取。
五、性能分析
性能分析工作量估算为 35 分钟。下面是程序实测时间。
在 macOS arm64、Python 3.9.6 下,固定 r=10 和种子 20261009,对同一批题目分别用列表和集合查重,各跑三次取中位数。
| 题目数量 | 列表查重耗时(秒) | 集合查重耗时(秒) |
|---|---|---|
| 100 | 0.007026 | 0.008159 |
| 1000 | 0.157539 | 0.069600 |
| 10000 | 10.710707 | 1.792698 |

一万道题时,集合查重的优势比较明显,耗时约为列表方案的六分之一。一百道题时差别不大,集合还略慢一些。列表方案用于对照,结果也会受后台负载影响。
cProfile 显示,主要时间花在建树和生成操作数上。另一轮运行中,一万道题的生成、写文件和批改分别用了 1.187 秒、0.191 秒和 0.892 秒。
六、测试结果
自动化测试共 38 项,全部通过。下面列出主要场景:
| 序号 | 测试内容 | 预期结果 | 结果 |
|---|---|---|---|
| 1 | -n 10 -r 10 |
生成 10 道题及对应答案 | 通过 |
| 2 | 缺少 -r |
显示帮助和参数错误 | 通过 |
| 3 | -n 0、-r 0 |
拒绝无效参数 | 通过 |
| 4 | 1/6 + 1/8 |
得到 7/24 |
通过 |
| 5 | 1/2 ÷ 3/4 |
得到 2/3 |
通过 |
| 6 | 加法或乘法交换两侧 | 判为重复 | 通过 |
| 7 | 3+(2+1) 与 1+2+3 |
判为重复 | 通过 |
| 8 | 1+2+3 与 3+2+1 |
判为不重复 | 通过 |
| 9 | 多个范围和随机种子 | 数值范围、运算符数量和中间结果合法 | 通过 |
| 10 | 生成一万道题 | 数量正确、题目不重复、答案一致 | 通过 |
| 11 | 使用生成答案进行批改 | 全部正确 | 通过 |
| 12 | 只改错第 2、5 题 | 错题号恰为 2、5 | 通过 |
| 13 | 未约分的等值答案 | 按数值判对 | 通过 |
| 14 | 空白作答或题号错位 | 分别判错或提示编号错误 | 通过 |
除这些用例外,还把生成的题面重新解析并计算答案。独立 AI 复核检查了一万道题和 32 种括号组合,均通过,日志放在 reports/agent-review/。

上图由实测日志整理,代理复核用时 192.15 秒。
Windows 打包后出现过中文输出编码错误,设置 UTF-8 后解决。修复后的 exe 也通过了一万道题的生成和批改测试。
七、运行方法
在项目目录执行:
# 生成 10 道题
python3 Myapp.py -n 10 -r 10
# 生成一万道题
python3 Myapp.py -n 10000 -r 10 --seed 20261009
# 批改作答
python3 Myapp.py -e Exercises.txt -a Answers.txt
# 运行测试
python3 tools/check.py
Windows 使用 Python 版时,可将 python3 换成 py -3;也可以下载 Myapp.exe,在终端中使用相同参数。再次生成题目会更新同名文件,需要保留多批题目时可指定 --output-dir。
八、项目小结
这次项目中,去重规则和括号处理比四则运算本身更值得注意。如果只看答案是否正确,很容易忽略题目结构已经变化。用表达式树处理后,这几部分比较容易联系起来。
目前程序能完成生成和批改,但题目难度还是随机的,也没有做图形界面。范围很小时,生成大量不同题目还需要较多尝试。这些地方后面还可以继续改进。

浙公网安备 33010602011771号