结对项目
| 这个作业属于哪个课程 | 软件工程 |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15703 |
| 这个作业的目标 | 实现一个自动生成小学四则运算题目的命令行程序 |
小组成员:吴圣林 3124004485 林锶堉3124004476
作业 GitHub 地址:https://github.com/morganalter/arithmetic-exercise-generator
一、动手之前的 PSP 预估
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
|---|---|---|
| Planning | 计划 | 30 |
| · Estimate | · 估计这个任务需要多少时间 | 30 |
| Development | 开发 | 480 |
| · Analysis | · 需求分析(包括学习新技术) | 60 |
| · Design Spec | · 生成设计文档 | 30 |
| · Design Review | · 设计复审 | 20 |
| · Coding Standard | · 代码规范 | 20 |
| · Design | · 具体设计 | 60 |
| · Coding | · 具体编码 | 180 |
| · Code Review | · 代码复审 | 40 |
| · Test | · 测试 | 70 |
| Reporting | 报告 | 120 |
| · Test Report | · 测试报告 | 60 |
| · Size Measurement | · 计算工作量 | 20 |
| · Postmortem & Process Improvement Plan | · 事后总结,并提出过程改进计划 | 40 |
| 合计 | 630 |
二、效能分析
2.1 性能优化耗时
本次程序性能优化共花费 40 分钟。
2.2 性能改进思路
最初版本的程序存在 3 个明显的性能问题,我们针对性做了三轮优化:
- 约束校验前置,减少无效递归
- 初始问题:先生成完整的表达式树,再整体校验减法非负、除法结果为分数的约束,不合格就整棵树直接丢弃,浪费了大量计算资源。
- 优化方案:改为递归生成子树后立即校验,只要左 / 右子树不满足约束,直接返回
None,上层不再继续生成另一半子树,大幅减少无效的递归和计算。
- 集合去重替代列表遍历
- 初始问题:用普通列表存储已生成的题目,每次判重都要遍历整个列表,时间复杂度为 O (n),题目数量越大速度越慢。
- 优化方案:改用 规范前缀串 + Python 集合(set)去重,集合的查找时间复杂度为 O (1)。
- 标准库替代手动分数运算
- 初始问题:手动编写分数的通分、约分、四则运算代码,不仅冗余,而且纯 Python 实现的运算效率低。
- 优化方案:直接使用 Python 内置的
fractions.Fraction标准库,运算效率远高于手写 Python 代码,同时自动保证计算精度。
2.3性能测试与分析图
我们使用 Python 官方性能分析工具cProfile对程序进行采样,测试条件:生成 10000 道题目,参数为 -n 10000 -r 20。
测试总结果:程序共执行 267 万次函数调用,总运行时间仅0.778 秒。
我们使用snakeviz工具生成了可视化的调用栈冰柱图:

如图为优化后程序性能分析冰柱图,可以看到:
- 最顶层蓝色条是程序总入口,总耗时 0.778 秒;
- 橙色的
generate_exercises是题目生成主函数,占据了绝大多数运行时间; - 最宽的红色条带是
_gen_expr(表达式递归生成函数),是整个程序中耗时占比最高的业务函数。
2.4 耗时最高的函数分析

如图,程序中消耗最大的函数:_gen_expr(表达式递归生成函数)
三.设计实现过程
本程序采用模块化设计,整体分为 3 大模块、1 个核心数据类。
3.1核心数据结构
采用二叉表达式树存储算术表达式:
- 叶子节点:存储数值,类型为
Fraction,统一表示自然数、真分数、带分数 - 内部节点:存储运算符(+、−、×、÷)以及左右子树指针
3.2题目生成主流程
- 解析命令行参数,校验参数合法性
- 随机选择当前题目的运算符数量
- 递归生成表达式树,同步校验约束条件
- 生成规范串,通过集合判重,重复则重新生成
- 转换为带括号的中缀字符串,计算答案
- 达到指定数量后,分别写入 Exercises.txt 和 Answers.txt
四.代码说明
4.1表达式树节点类
统一表示表达式的叶子和内部节点,通过is_leaf方法区分节点类型。
class ExprNode:
def init(self, value=None, op=None, left=None, right=None):
self.value = value # 叶子节点存数值(Fraction)
self.op = op # 内部节点存运算符
self.left = left # 左子树
self.right = right # 右子树
def is_leaf(self):
return self.value is not None
4.2递归生成表达式树
核心生成函数,通过递归拆分运算符数量,生成后立即校验约束,不满足则返回 None 由上层重试,保证生成的题目全部符合规则。
def _gen_expr(op_cnt, r):
if op_cnt == 0:
return gen_number(r)
ops = ['+', '-', '×', '÷']
op = random.choice(ops)
# 随机分配运算符数量到左右子树
left_ops = random.randint(0, op_cnt - 1)
right_ops = op_cnt - 1 - left_ops
left = _gen_expr(left_ops, r)
right = _gen_expr(right_ops, r)
if left is None or right is None:
return None
l_val = evaluate(left)
r_val = evaluate(right)
# 校验减法非负
if op == '-':
if l_val < r_val:
return None
# 校验除法结果为分数
elif op == '÷':
if r_val == 0:
return None
res = l_val / r_val
if res.denominator == 1:
return None
return ExprNode(op=op, left=left, right=right)
4.3规范串生成
后序遍历生成前缀表达式,对交换律运算符按字典序排序子节点,确保等价题目生成相同字符串,是去重功能的核心逻辑。
def get_canonical(node):
if node.is_leaf():
return format_fraction(node.value)
l = get_canonical(node.left)
r = get_canonical(node.right)
# +和×交换左右序,生成唯一规范串
if node.op in ('+', '×'):
if l > r:
l, r = r, l
return f"({node.op} {l} {r})"
4.4题目批改核心
逐题读取题目和用户答案,分别计算正确值并比对,统计对错题号,最终按要求格式输出批改结果。
def grade(exercise_file, answer_file):
with open(exercise_file, 'r', encoding='utf-8') as f:
exercises = [line.strip() for line in f if line.strip()]
with open(answer_file, 'r', encoding='utf-8') as f:
user_ans = [line.strip() for line in f if line.strip()]
correct = [] # 存答对的题号
wrong = [] # 存答错的题号
for i in range(len(exercises)):
ex = exercises[i].rstrip('=').strip()
ua = user_ans[i]
correct_val = eval_expr_str(ex)
user_val = parse_fraction(ua)
if correct_val == user_val:
correct.append(i + 1)
else:
wrong.append(i + 1)
with open('Grade.txt', 'w', encoding='utf-8') as f:
f.write(f"Correct: {len(correct)} ({', '.join(map(str, correct))})\n")
f.write(f"Wrong: {len(wrong)} ({', '.join(map(str, wrong))})\n")
五.测试运行
5.1
共设计 12 组测试用例,覆盖参数校验、功能约束、边界场景、性能、批改五大类,全部通过。
| 编号 | 测试类别 | 测试内容 | 输入 / 操作 | 预期结果 | 测试结果 |
|---|---|---|---|---|---|
| 1 | 参数校验 | 缺失 - r 参数 | python main.py -n 10 | 报错并打印帮助信息 | 通过 |
| 2 | 参数校验 | -r 为 0 | python main.py -n 5 -r 0 | 提示 - r 必须为正整数 | 通过 |
| 3 | 功能验证 | 单题生成 | python main.py -n 1 -r 10 | 生成 1 道合法题目,答案正确 | 通过 |
| 4 | 约束校验 | 减法非负 | 生成 100 道题逐题检查 | 所有减法子表达式左值≥右值,结果非负 | 通过 |
| 5 | 约束校验 | 除法结果为分数 | 生成 100 道题逐题检查 | 所有除法结果分母均不为 1 | 通过 |
| 6 | 去重验证 | 小范围生成 | python main.py -n 50 -r 5 | 50 道题规范串全部唯一,无重复 | 通过 |
| 7 | 格式验证 | 分数输出格式 | 查看 Exercises.txt | 真分数 a/b,带分数 i’f/d,格式符合要求 | 通过 |
| 8 | 格式验证 | 括号正确性 | 生成含 3 个运算符的题目 | 括号符合优先级与左结合规则,无多余括号 | 通过 |
| 9 | 批改功能 | 全对批改 | 用生成的 Answers.txt 批改 | Correct: 总数,Wrong: 0 | 通过 |
| 10 | 批改功能 | 错题批改 | 修改 3 道题答案后批改 | 准确统计对错题号,数量与修改一致 | 通过 |
| 11 | 性能测试 | 万道题生成 | python main.py -n 10000 -r 20 | 无重复题目 | 通过 |
| 12 | 边界测试 | 带分数运算 | 构造1’1/2 + 1/2 = | 答案为2,计算正确 | 通过 |
5.2正确性说明:
测试覆盖了所有需求点:参数处理合法、生成题目全部满足数值范围、减法非负、除法结果为分数、运算符不超过 3 个、题目不重复等约束;批改功能可准确判分并统计题号;万级题目生成性能达标。因此可以确定程序功能正确,符合全部需求。
六.PSP 表格(实际耗时)
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 25 |
| · Estimate | ・估计这个任务需要多少时间 | 30 | 25 |
| Development | 开发 | 480 | 510 |
| · Analysis | ・需求分析(包括学习新技术) | 60 | 70 |
| · Design Spec | ・生成设计文档 | 30 | 25 |
| · Design Review | ・设计复审 (和同事审核设计文档) | 20 | 15 |
| · Coding Standard | ・代码规范 (为目前的开发制定合适的规范) | 20 | 10 |
| · Design | ・具体设计 | 60 | 50 |
| · Coding | ・具体编码 | 180 | 220 |
| · Code Review | ・代码复审 | 40 | 40 |
| · Test | ・测试(自我测试,修改代码,提交修改) | 70 | 80 |
| Reporting | 报告 | 120 | 110 |
| · Test Report | ・测试报告 | 60 | 50 |
| · Size Measurement | ・计算工作量 | 20 | 15 |
| · Postmortem & Process Improvement Plan | ・事后总结,并提出过程改进计划 | 40 | 45 |
| 合计 | 630 | 645 |
七.项目小结
7.1项目成果
本次结对项目完整实现了全部需求:
- 支持
-n控制题目数量、-r控制数值范围,参数缺失或非法时有明确报错 - 生成的题目满足减法非负、除法结果为分数、运算符不超过 3 个等所有约束
- 实现了基于交换律的题目去重,保证题目不重复
- 支持自然数、真分数、带分数的运算与格式输出
- 具备题目批改功能,可统计对错数量与题号并输出 Grade.txt
- 性能支持万道题目稳定生成
7.2经验与教训
数据结构选对事半功倍。最初考虑过直接字符串拼接生成题目,但括号处理和去重都非常麻烦。改用表达式树后,求值、去重、格式化都可以通过递归优雅实现,代码量和复杂度大幅降低。
同时要注重边界测试,-r=1、-r=2等小数值范围场景,最容易暴露重试逻辑、除法约束等方面的 bug,是测试的重中之重,不能只测常规范围。
7.3结对感受
- 吴圣林:两个人讨论需求和方案时,能快速发现彼此的思维盲区,比如去重规则的边界情况,一个人很容易考虑不周。代码复审阶段也能快速发现笔误和逻辑漏洞,比单人自查效率高很多。
- 林锶堉:从对方身上学到了很多编码习惯,比如变量命名规范、边界条件处理的思路。

浙公网安备 33010602011771号