软件工程结对项目小学四则运算自动生成程序
项目成员
姓名:王昊圆,学号:3224004232
姓名:苏拉娅・斯地克阿吉,学号:3224004384
Github 项目地址:https://github.com/wang1580/four-arithmetic
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 10 | 10 |
| · Estimate | 估计这个任务需要多少时间 | 20 | 20 |
| Development | 开发 | 60 | 60 |
| · Analysis | 需求分析 (包括学习新技术) | 20 | 30 |
| · Design Spec | 生成设计文档 | 20 | 30 |
| · Design Review | 设计复审 (和同事审核设计文档) | 10 | 10 |
| · Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 10 | 20 |
| · Design | 具体设计 | 40 | 40 |
| · Coding | 具体编码 | 30 | 40 |
| · Code Review | 代码复审 | 10 | 20 |
| · Test | 测试(自我测试,修改代码,提交修改) | 20 | 20 |
| Reporting | 报告 | 20 | 30 |
| · Test Report | 测试报告 | 20 | 20 |
| · Size Measurement | 计算工作量 | 20 | 20 |
| · Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 15 | 10 |
| 合计 | 325 | 380 |
效能分析
本次针对四则运算题目生成程序进行性能调优,改进程序性能总共耗时约 40 分钟,分为性能采样、定位瓶颈、代码优化、验证效果四个步骤。
性能分析结果
使用 Python 内置cProfile工具采集程序运行数据,并用 SnakeViz 可视化性能报告,性能截图如下:
本次测试生成 10000 道四则运算题目,程序总运行耗时为 0.527 s,和截图顶部builtins.exec总耗时一致。
从 Icicle 冰柱图可以直观看到函数调用栈与耗时占比:消耗时间最多的自定义函数是normalize_expr(表达式标准化函数),也就是图中橙色区块对应的函数,为本程序的性能瓶颈。normalize_expr内部嵌套的递归norm函数,反复执行 token 分割、列表拼接与操作数比较,占用了程序绝大部分自定义代码 CPU 耗时。

性能改进思路
- 优化前的问题
项目早期版本做题目去重时,没有做表达式标准化。每生成一道新题目,需要和之前所有题目字符串逐个对比,时间复杂度为(O(n^2))。当题目数量变大时,比对次数急剧增加,程序运行缓慢。同时加法、乘法存在交换律,1+2和2+1属于等价题目,旧方法无法识别这类重复题目。 - 优化方案
新增normalize_expr表达式标准化函数,递归解析表达式,利用加法、乘法交换律,交换操作数顺序,让等价表达式转换成完全相同的字符串。使用集合set保存已经生成过的标准化表达式。集合查找的时间复杂度为(O(1)),不再需要和全部历史题目逐一比对,大幅降低去重开销。 - 优化效果验证
优化完成后,使用cProfile采集性能数据。生成 10000 道四则运算题目耗时 0.527 秒,程序运行速度得到明显提升。性能瓶颈转移到normalize_expr函数,原因是表达式标准化需要递归解析表达式并处理字符串,存在一定开销。如果后续还需要继续提升性能,可以优化递归逻辑、减少字符串拼接操作。
设计实现过程
代码组织
本项目使用 Python 语言开发,采用函数式编程,没有自定义类,整体划分为 5 个核心函数模块,各函数之间相互调用配合完成全部功能:
-
分数处理模块:基于 Python 内置
Fraction类,实现自然数、真分数、带分数的格式化输出,支持2’3/8格式转换;保证减法不会产生负数,除法运算结果为真分数。 -
表达式生成模块:随机生成算术表达式,控制每道题目运算符数量不超过 3 个;支持随机插入括号;生成表达式时做约束校验,减法保证被减数大于等于减数,除法结果为真分数。
-
表达式标准化与去重模块:递归遍历表达式抽象语法树,对加法、乘法子表达式进行排序,生成唯一签名;利用集合实现题目去重,满足作业要求:交换 +、× 左右操作数视为同一道题目。
-
命令行参数解析模块:解析
-n、-r、-e、-a四种参数。-r参数必须传入,缺少-r时程序输出帮助信息并终止运行。程序分为两种运行模式:题目生成模式、判题模式。 -
判题统计模块:读取
Exercises.txt题目文件与Answers.txt答案文件,逐行解析题目并计算标准答案,和用户答案比对;收集正确、错误题号,按照指定格式输出到Grade.txt。 -
解析命令行参数,判断运行模式:
- 生成模式:
-n指定题目数量,-r指定数值范围; - 判题模式:
-e指定题目文件路径,-a指定答案文件路径。
- 生成模式:
-
生成模式:循环生成表达式,校验表达式合法性并做去重;题目写入
Exercises.txt,对应答案写入Answers.txt。 -
判题模式:读取题目文件与答案文件,逐题计算标准答案并比对;统计正确、错误题号,按照固定格式写入
Grade.txt。
代码说明
1. 分数格式化函数:实现真分数、带分数输出
from fractions import Fraction
def frac_to_str(f: Fraction) -> str:
"""将Fraction对象转换为作业要求字符串格式:3/5 或者 2'3/8"""
if f.denominator == 1:
return str(f.numerator)
int_part = f.numerator // f.denominator
frac_num = f.numerator % f.denominator
if int_part == 0:
return f"{frac_num}/{f.denominator}"
else:
return f"{int_part}'{frac_num}/{f.denominator}"
2. 表达式标准化签名函数:实现题目去重
def normalize_expr(expr):
"""递归生成表达式唯一签名,加法、乘法交换操作数得到相同签名"""
expr = expr.replace("( ","").replace(" )","")
tokens = expr.split()
def norm(tok_list):
if len(tok_list)==1:
return [tok_list[0]]
mid = 1
left = tok_list[:mid]
op = tok_list[mid]
right = tok_list[mid+1:]
l_norm = norm(left)
r_norm = norm(right)
if op in ('+','×'):
if l_norm > r_norm:
l_norm, r_norm = r_norm, l_norm
return l_norm + [op] + r_norm
return " ".join(norm(tokens))
3.判题模块核心函数:比对答案并输出 Grade.txt
def grade(ex_file, ans_file):
with open(ex_file,"r",encoding="utf-8") as f:
ex_lines = [line.strip() for line in f if line.strip()]
with open(ans_file,"r",encoding="utf-8") as f:
ans_lines = [line.strip() for line in f if line.strip()]
correct = []
wrong = []
for idx, (ex, std_ans) in enumerate(zip(ex_lines, ans_lines), start=1):
std_frac = str_to_frac(std_ans)
try:
user_frac = str_to_frac(ans_lines[idx-1])
if user_frac == std_frac:
correct.append(idx)
else:
wrong.append(idx)
except:
wrong.append(idx)
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")
## 测试运行
本次共设计 10 组测试用例,覆盖全部需求点,验证程序正确性:
1. **用例 1:基础题目生成** `python four.py -n 10 -r 10`
预期结果:生成 10 道四则运算题目写入 Exercises.txt,答案写入 Answers.txt;每道题目运算符≤3;减法子表达式不会出现负数;除法结果为真分数。
2. **用例 2:缺少 - r 参数测试** `python four.py -n 10`
预期结果:程序检测到缺少`-r`参数,输出帮助信息并报错退出。
3. **用例 3:大批量题目生成** `python four.py -n 10000 -r 100`
预期结果:稳定生成 10000 道不重复题目,程序不崩溃,题目全部满足约束条件。
4. **用例 4:加法交换去重测试**
预期结果:不会同时生成`2 + 3 =` 和 `3 + 2 =`,二者视为同一题目,自动去重。
5. **用例 5:乘法交换去重测试**
预期结果:不会同时生成`2 × 3 =` 和 `3 × 2 =`,二者视为同一题目,自动去重。
6. **用例 6:判题全对测试**
先生成 10 道题目,再执行判题命令`python four.py -e Exercises.txt -a Answers.txt`。预期 Grade.txt:`Correct: 10 (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)`,Wrong 为空。
7. **用例 7:手动制造错题判题测试**
手动修改 Answers.txt 中第 3 题对应的答案,执行判题。预期 Grade.txt 中 Wrong 列表包含题号 3。
8. **用例 8:真分数运算测试**
生成题目`1/6 + 1/8 =`,标准答案输出`7/24`,分数计算准确无误。
9. **用例 9:带分数格式测试**
生成带分数表达式,输出格式为`2’1/2`,答案同样支持带分数格式解析与输出。
10. **用例 10:括号与多运算符测试**
生成最多 3 个运算符、带括号的复合表达式,表达式计算结果正确,括号逻辑合法。
## 项目小结
本次结对项目完成小学四则运算命令行程序,支持自然数、真分数、带分数、括号表达式,支持题目去重功能以及自动判题统计。
结对开发分工安排:王昊圆负责表达式随机生成模块、分数运算模块的代码编写与单元测试;苏拉娅・斯地克阿吉负责命令行参数解析、表达式签名去重、判题模块开发,同时完成文档撰写与博客整理。开发过程中两人互相进行代码复审,共同排查边界 bug,例如减法负数、除法结果合法性、交换律题目重复等问题。
### 项目收获
1. 掌握命令行参数解析、文件读写、分数精确运算,规避浮点数运算带来的精度丢失问题。
2. 理解表达式抽象语法树、表达式归一化签名思路,解决加法、乘法交换带来的题目重复判定难题。
3. 体会结对编程优势:双人互相审查代码,单人容易忽略的边界逻辑问题,通过讨论可以快速发现并修复。
### 项目不足与改进方向
当前表达式随机生成逻辑偶尔会生成多余冗余括号,后续可以优化括号生成策略;一万道题目虽然经过性能优化,但随机生成逻辑仍有优化空间,可以进一步提升生成速度。
### 结对互评
- 王昊圆:苏拉娅・斯地克阿吉在代码复审阶段非常细心,能够快速发现边界条件漏洞;建议后续项目在需求分析阶段就一起讨论,更早划分模块任务。
- 苏拉娅・斯地克阿吉:王昊圆负责的分数运算模块稳定可靠,调试能力强;建议在调试过程中增加更多中间日志打印,方便快速定位异常。
### 结对感受
本次结对开发让我们真切体会结对编程的价值。在需求分析阶段两人一起研读题目要求,避免单方面理解需求出现偏差;代码编写阶段分工开发,代码复审阶段互相检查代码缺陷。很多单人写代码容易忽略的边界条件,通过双人讨论能够提前发现并修复。同时我们也学会了合理分配任务、沟通协调,是一次很好的实践锻炼。

浙公网安备 33010602011771号