结对作业
小学四则运算题目生成与批改程序
0. 成员与项目信息
| 项目 | 信息 |
|---|---|
| 成员 1 | 姓名:李智杰 学号:3124004212 |
| 成员 2 | 姓名:黄俊达 学号:3124004207 |
| GitHub 项目地址 | Zikie222/3124004212结对作品 |
| 开发语言 | Python 3(兼容 Python 3.6) |
| 主要交付物 | arithmetic_app.py、arithmetic_app_optimized.py、README |

1. 项目背景与需求分析
本项目实现一个自动生成小学四则运算题目的命令行程序,并支持对学生答案进行批改。作业要求不仅包括基本的加、减、乘、除运算,还要求处理真分数、带分数、括号、题目去重以及批改统计等问题。
程序需要支持两种工作模式:
- 题目生成模式:使用
-n指定题目数量,使用-r指定自然数、分子和分母的范围。 - 批改模式:使用
-e指定题目文件,使用-a指定答案文件,并生成Grade.txt。
核心约束如下:
- 生成结果不能为负数;
- 除法表达式的结果必须是真分数;
- 每道题最多包含 3 个运算符;
- 一次运行中不能生成通过交换律、结合律可以相互转换的等价题目;
- 题目和答案分别写入
Exercises.txt、Answers.txt; - 支持至少一万道题目的生成;
- 批改结果写入
Grade.txt,列出正确和错误题号。
例如:
python arithmetic_app_optimized.py -n 10 -r 10
python arithmetic_app_optimized.py -e Exercises.txt -a StudentAnswers.txt
带分数使用作业规定的格式,例如 2'3/8 表示 (2+3/8)。
2. PSP 过程记录
下面的表格记录项目各阶段的计划时间和实际时间。实际时间应根据两位同学的 PSP 记录补充;性能分析中的实测运行时间已经填入。
| 阶段 | 主要工作 | 预计用时/小时 | 实际用时/小时 |
|---|---|---|---|
| 需求分析 | 阅读作业要求,整理参数、文件格式和约束 | 1.0 | 2.5 |
| 总体设计 | 设计表达式树、分数表示和命令行模式 | 1.5 | 2.0 |
| 题目生成 | 实现随机数、运算符、括号和结果约束 | 3.0 | 4 |
| 等价题去重 | 实现交换律、结合律规范化键 | 2.0 | 2.0 |
| 文件输出 | 实现 Exercises、Answers 文件生成 | 1.0 | 1.5 |
| 批改功能 | 实现表达式解析、答案比较和 Grade 输出 | 3.0 | 3.5 |
| 兼容性修改 | 调整为 Python 3.6 可运行版本 | 1.5 | 3.5 |
| 性能分析 | 使用 VS Community Profiler 定位热点 | 1.0 | 2.0 |
| 性能优化 | 缓存表达式计算结果并重新测试 | 1.5 | 2.0 |
| 测试与文档 | 完成功能测试、README 和博客 | 2.0 | 1.0 |
| 合计 | 16.5 | 24 |
3. 设计与实现过程
3.1 总体结构
程序主要由以下模块组成:
| 模块/函数 | 作用 |
|---|---|
Expr |
表示数字节点和四则运算节点 |
ExerciseGenerator |
生成随机表达式树,并检查结果约束 |
evaluate |
使用 Fraction 精确计算表达式 |
canonical |
为加法、乘法按交换律和结合律生成唯一键 |
format_expr |
将表达式树输出为带括号的题目文本 |
Parser |
不使用 eval,解析题目和学生答案 |
grade_files |
比较答案并输出正确、错误题号 |
main |
处理命令行参数并选择工作模式 |
3.2 表达式树
表达式没有直接拼接字符串,而是先构造树结构。叶子节点保存自然数或分数,非叶子节点保存运算符和左右子树。
×
/ \
3 +
/ \
1 2
使用表达式树的好处是:
- 可以递归计算结果;
- 可以根据运算优先级自动补括号;
- 可以统计运算符数量;
- 可以为等价题目生成规范化键;
- 优化时可以在节点中缓存计算结果。
3.3 分数与结果约束
程序使用标准库 fractions.Fraction 保存分数,避免浮点数误差。生成减法时,如果左值小于右值就交换左右子树;生成除法时,要求除数不为零且结果小于 1,从而满足“除法结果是真分数”的要求。
输出时:
- 分母为 1,输出整数;
- 分子小于分母,输出
a/b; - 分子大于分母,输出
整数'分子/分母。
3.4 等价题目去重
对于加法和乘法,程序递归展开同类运算并排序子表达式,因此:
2 + 3
3 + 2
(1 + 2) + 3
1 + (2 + 3)
可以得到相同的规范化键。减法和除法不满足交换律,因此保留左右顺序。生成过程中将规范化键放入集合 seen,重复键直接丢弃。
3.5 命令行与文件流程
4. 关键代码说明
4.1 生成表达式并约束结果
if op == "-" and a < b:
left, right = right, left
elif op == "÷":
if b == 0 or not a < b:
left, right = right, left
if b == 0 or not a < b:
op = "×"
这段逻辑保证减法结果不为负,并尽量保证除法结果是真分数。如果随机生成的两个子树不适合做除法,就改用乘法,避免产生非法题目。
4.2 优化前的重复计算问题
优化前,生成过程中会多次调用 evaluate():生成节点时计算一次,检查结果时可能再次计算,写入答案时还会重新计算。表达式树包含多个运算节点时,这会造成重复递归遍历。
4.3 优化后的结果缓存
优化版在 Expr 节点中增加 cached 字段:
def evaluate(expr):
if expr.kind == "num":
return expr.value
if expr.cached is not None:
return expr.cached
a = evaluate(expr.left)
b = evaluate(expr.right)
result = calculate(expr.kind, a, b)
expr.cached = result
return result
创建运算节点时也会把已经得到的结果传入节点。这样后续再次求值时可以直接返回 cached,减少递归和 Fraction 运算。
5. 测试过程与结果
5.1 功能测试用例
| 编号 | 测试内容 | 操作/输入 | 预期结果 |
|---|---|---|---|
| 1 | 生成 10 道题 | -n 10 -r 10 |
生成两个文本文件 |
| 2 | 生成 1000 道题 | -n 1000 -r 100 |
成功生成 1000 道不重复题 |
| 3 | 固定随机种子 | --seed 2026 |
多次运行结果一致 |
| 4 | 非法数量 | -n -1 -r 10 |
报错并提示参数错误 |
| 5 | 非法范围 | -n 10 -r 0 |
报错并提示范围错误 |
| 6 | 检查负数结果 | 随机生成减法 | 所有结果大于等于 0 |
| 7 | 检查除法结果 | 随机生成除法 | 除法结果为真分数且不除零 |
| 8 | 检查运算符数 | 随机生成表达式 | 每道题不超过 3 个运算符 |
| 9 | 正确答案批改 | 使用生成的 Answers.txt |
全部判定为 Correct |
| 10 | 错误答案批改 | 修改部分答案 | 正确、错误题号统计正确 |
| 11 | 带分数解析 | 输入 2'3/8 |
等价于 19/8 |
| 12 | 括号和优先级 | 输入 3 × (1 + 2) |
结果为 9 |
5.2 批改测试
程序生成题目后,使用生成的标准答案进行批改,结果为:
Correct: 1000 (...)
Wrong: 0 ()
随后手动修改部分答案,再次运行批改程序,错误题号能够被列出。解析器支持整数、真分数、带分数、括号以及 ×、÷ 运算符,并且不调用 Python 的 eval。



6. 性能分析与优化
6.1 分析环境
- 工具:Visual Studio Community 2017 Python Profiler;
- Python:Python 3.6(32-bit);
- 参数:
-n 1000 -r 100 --seed 2026; - 对比文件:
arithmetic_app.py与arithmetic_app_optimized.py。
6.2 优化前分析
优化前总运行时间为 0.076 秒。热点函数主要包括:
| 函数 | 非独占时间占比 | 说明 |
|---|---|---|
ExerciseGenerator.generate |
43.68% | 生成、约束检查和去重的主流程 |
ExerciseGenerator.make_tree |
26.09% | 递归创建表达式树 |
evaluate |
15.89% | 递归计算表达式结果 |
write_generated_files |
16.69% | 生成题目和答案文件 |
canonical |
4.97% | 生成等价题规范化键 |
从分析结果看,evaluate 在生成、检查和写答案过程中被重复调用,是可以优化的部分。


6.3 优化方案
优化版没有改变题目规则和输出格式,而是在表达式节点中缓存 Fraction 结果:
- 子节点创建后立即计算结果;
- 父节点直接使用子节点结果;
evaluate()再次调用时优先读取缓存;- 写入
Answers.txt时直接使用缓存结果。
这种优化属于记忆化/缓存优化,减少了重复递归求值和重复的分数运算,代码改动小,且不会影响答案正确性。
6.4 优化后分析
优化后总运行时间为 0.069 秒。与优化前相比:
时间减少 = 0.076 - 0.069 = 0.007 秒
下降比例 ≈ 0.007 / 0.076 × 100% ≈ 9.2%
优化后报告中,evaluate 不再出现在主要热点函数列表中,热点主要集中在表达式树生成和 Python 模块加载上,说明缓存策略减少了重复求值开销。
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 总运行时间 | 0.076 秒 | 0.069 秒 | 减少约 9.2% |
evaluate |
15.89% | 不再是主要热点 | 明显降低 |
| 主要算法热点 | generate、make_tree、evaluate |
generate、make_tree |
求值开销下降 |


6.5 优化结论
本次优化没有盲目修改所有函数,而是先通过 Profiler 找到热点,再针对重复求值进行改进。优化后功能测试仍然全部通过,生成结果和批改结果保持一致,运行时间下降约 9.2%。对于当前规模,Python 启动、模块导入和文件系统访问仍占一定比例;如果继续优化,可以考虑批量写入、减少模块导入开销以及改进大规模题目去重策略。
7. 项目总结
本项目完成了从需求分析、数据结构设计、代码实现、兼容性处理、功能测试到性能优化的完整过程。
7.1 项目收获
- 学会使用表达式树组织具有层次结构的算术表达式;
- 学会使用
Fraction处理精确分数; - 理解交换律、结合律和规范化键在去重中的作用;
- 通过自定义解析器避免直接使用
eval; - 学会使用 Visual Studio Profiler 观察函数调用和耗时;
- 认识到性能优化应以实际分析结果为依据,而不是凭感觉修改代码。
7.2 合作与改进
结对开发过程中,一位同学主要负责需求梳理、测试和文档,另一位同学主要负责代码实现、性能分析和优化。通过 GitHub 提交记录同步代码和文档,并通过固定随机种子复现实验结果。
项目仍有可以改进的地方:题目范围很小时,去重候选空间会变小;题目数量非常大时,集合占用的内存会增加;当前输出仍是文本格式,后续可以增加图形界面或导出更多格式。
8. 项目运行说明
生成题目
python arithmetic_app_optimized.py -n 10 -r 10
固定随机种子生成
python arithmetic_app_optimized.py -n 1000 -r 100 --seed 2026
批改答案
python arithmetic_app_optimized.py -e Exercises.txt -a Answers.txt
生成文件:
Exercises.txt # 题目
Answers.txt # 标准答案
Grade.txt # 批改统计
完整代码请见:GitHub 项目仓库。
浙公网安备 33010602011771号