Zki

结对作业

小学四则运算题目生成与批改程序

0. 成员与项目信息

项目 信息
成员 1 姓名:李智杰 学号:3124004212
成员 2 姓名:黄俊达 学号:3124004207
GitHub 项目地址 Zikie222/3124004212结对作品
开发语言 Python 3(兼容 Python 3.6)
主要交付物 arithmetic_app.py、arithmetic_app_optimized.py、README

4134bc89863a36dc51d5cebfb0c1ce23


1. 项目背景与需求分析

本项目实现一个自动生成小学四则运算题目的命令行程序,并支持对学生答案进行批改。作业要求不仅包括基本的加、减、乘、除运算,还要求处理真分数、带分数、括号、题目去重以及批改统计等问题。

程序需要支持两种工作模式:

  1. 题目生成模式:使用 -n 指定题目数量,使用 -r 指定自然数、分子和分母的范围。
  2. 批改模式:使用 -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 命令行与文件流程

flowchart TD A[读取命令行参数] --> B{工作模式} B -->|生成模式| C[随机生成表达式树] C --> D[检查结果和运算符数量] D --> E[规范化键去重] E --> F[写入 Exercises.txt 和 Answers.txt] B -->|批改模式| G[读取题目和学生答案] G --> H[Parser 解析表达式] H --> I[Fraction 精确比较] I --> J[写入 Grade.txt]

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。

63333c1fc51eb8ac0b12c5c5c6b574a7
746b8630f34665944c72e211838fda5f
9b48762f286b6b19af0aa35a5c29a431


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 在生成、检查和写答案过程中被重复调用,是可以优化的部分。

4101438b87800b3831088be1a4ecf1eb
1bf8e747ce0772c249a67b065ce23863

6.3 优化方案

优化版没有改变题目规则和输出格式,而是在表达式节点中缓存 Fraction 结果:

  1. 子节点创建后立即计算结果;
  2. 父节点直接使用子节点结果;
  3. evaluate() 再次调用时优先读取缓存;
  4. 写入 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 求值开销下降

8cbfef36bad76c8d23c9aef0f94148d1
34dd48adfcc1d1ad6f702b2d2cd8c6a9

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 项目仓库。

posted on 2026-09-22 15:40  Zki  阅读(10)  评论(0)    收藏  举报

导航