| 这个作业属于哪个课程 | 【计科6】 |
|---|---|
| 这个作业要求在哪里 | 个人项目:自动生成小学四则运算题目 |
| 这个作业的目标 | 独立完成命令行运算程序的开发全流程,掌握 AST(抽象语法树)设计、性能分析(cProfile+SnakeViz)、自动化测试及项目文档编写。 |
作者信息:
- 姓名:【杨宗林】
- 学号:【3124004146】
- Github项目地址:【 https://github.com/treec34/treec34/tree/main/math 】
1. PSP2.1 表格
在项目初期,我制定了个人软件过程(PSP)规划表以追踪项目的时间开销。在实际开发中,由于使用了抽象语法树(AST)重构生成逻辑,在“具体编码”和“测试”环节花费了比预期更多的时间。
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 20 |
| · Estimate | · 估计这个任务需要多少时间 | 30 | 20 |
| Development | 开发 | 450 | 510 |
| · Analysis | · 需求分析 (包括学习新技术) | 60 | 50 |
| · Design Spec | · 生成设计文档 | 40 | 30 |
| · Design Review | · 设计复审 | 20 | 15 |
| · Coding Standard | · 代码规范 | 10 | 10 |
| · Design | · 具体设计 | 60 | 90 |
| · Coding | · 具体编码 | 120 | 150 |
| · Code Review | · 代码复审 | 30 | 25 |
| · Test | · 测试(自我测试,修改代码,提交修改) | 110 | 140 |
| Reporting | 报告 | 60 | 70 |
| · Test Report | · 测试报告 | 30 | 40 |
| · Size Measurement | · 计算工作量 | 10 | 10 |
| · Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 20 |
| 合计 | 540 | 600 |
2. 效能分析
在完成初代版本后,我尝试生成 10000 道题目。为了探查性能瓶颈,我使用了 Python 内置的 cProfile 结合 snakeviz 库生成了可视化的冰柱图(Icicle plot)。
性能分析图展示:

改进思路与分析:
从性能图谱中可以清晰地看到,程序消耗最大的函数是 generate_ast(抽象语法树生成逻辑)以及底层的 generate_random_fraction(随机分数生成)。
在初期版本中,由于存在大量不合规的除法节点(除数为0或结果大于1)以及产生负数的减法节点,导致树的递归生成频繁触发 continue 重试。
- 改进方案:针对减法可能产生负数的问题,我取消了粗暴的丢弃重试,改为直接交换左右子节点 (
left, right = right, left),极大地减少了无效递归。针对去重,设计了get_canonical特征序列化函数,利用 Set 的 $O(1)$ 哈希查询代替了对树结构的深层遍历比对,确保了 1 万道题在 1 秒左右即可生成完毕。
3. 设计实现过程
本项目采用面向对象编程与抽象语法树(AST)的架构体系。核心设计如下:
- 类结构: 定义了
Node类,代表 AST 中的每一个节点。非叶子节点存储op(运算符)、left和right,叶子节点直接存储value(基于fractions.Fraction实例)。 - 核心函数拆解:
generate_ast(num_ops, max_val): 递归生成带有运算约束的 AST。get_canonical(node): 遍历 AST,针对满足交换律的加法和乘法实施子节点排序,生成标准化标识符,用以判定等价题目(例如1+2+3和3+(1+2)特征码相同)。parse_and_eval(expr_str): 判卷模块的核心,利用正则将复杂的带分数形式a'b/c转换为 Pythoneval()可计算的字符串表达式进行比对。
- 流程关系: 命令行
argparse解析输入 -> 循环调用generate_ast拿到数学树结构 ->get_canonical去重检测 ->to_string()添加必要括号并格式化输出 -> 写入文件。
4. 代码说明
以下展示项目中最为关键的 AST 核心生成逻辑,它负责拦截所有非法的数学逻辑并确保运算合法:
def generate_ast(num_ops, max_val):
"""递归生成合法的抽象语法树"""
# 递归终止条件:生成叶子节点(随机分数/整数)
if num_ops == 0:
return Node(value=generate_random_fraction(max_val))
op = random.choice(OPERATORS)
# 随机分配左右子树的运算符数量
left_ops = random.randint(0, num_ops - 1)
right_ops = num_ops - 1 - left_ops
while True:
left = generate_ast(left_ops, max_val)
right = generate_ast(right_ops, max_val)
# 计算当前节点的值,并验证强制性约束条件
if op == '+':
val = left.value + right.value
elif op == '-':
if left.value < right.value:
# 核心改进:遇到负数直接交换子节点,避免废弃整棵树重试
left, right = right, left
val = left.value - right.value
elif op == '×':
val = left.value * right.value
elif op == '÷':
if right.value == 0:
continue # 分母为0,触发重试
val = left.value / right.value
# 业务约束:除法子表达式的结果必须为严格真分数(<1)
if val >= 1:
continue
# 缓存计算结果,避免后续 to_string 时的重复求值
node = Node(op=op, left=left, right=right)
node.value = val
return node
5. 测试运行
我编写了多个测试用例,覆盖了系统不同边界条件的业务流。以下共享 10 个具有代表性的程序测试场景与结果:
| 测试场景 | 命令行指令 / 内部逻辑 | 预期结果与说明 |
|---|---|---|
| 1. 基础题库生成 | main.py -n 10 -r 10 |
正常生成 10 道题在 Exercises.txt,所有参与计算的数值均小于 10。 |
| 2. 万级数据压力测试 | main.py -n 10000 -r 10 |
程序在 1 秒内顺利完成生成,无超时,证明 Set 去重性能达标。 |
| 3. 非法参数拦截 | main.py -n -5 -r 10 |
报错:范围和数量必须大于0,有效拦截非法输入。 |
| 4. 真分数化简格式化 | AST叶子计算出 25/10 |
返回 2'1/2,验证带分数的约分与格式化功能完全正确。 |
| 5. 乘除括号智能添加 | AST结构: 3 × (2 + 1) |
输出 3 × (2 + 1) = ,验证子节点运算符优先级判断正确。 |
| 6. 减法结合律去歧义 | AST结构: 5 - (2 + 1) |
输出 5 - (2 + 1) = ,避免输出 5 - 2 + 1 导致计算歧义。 |
| 7. 去重(交换律拦截) | 左树 4+5, 右树 5+4 |
两者被 get_canonical 解析为同一特征串,抛弃重复题。 |
| 8. 防止产生负数 | 减法节点 2 - 5 被选中 |
generate_ast 内部捕捉到负数,智能交换子节点变为 5 - 2。 |
| 9. 严格真分数限制 | 运算节点 5 ÷ 2 |
系统检测到除法结果为 2.5 >= 1,触发约束机制废弃重试。 |
| 10. 判卷全链路功能 | main.py -e Ex... -a Ans... |
对生成的 1 万道题以及修改的错题进行判卷,精确分拣 Correct: 和 Wrong: 数量并保存至 Grade.txt。 |

正确性说明:
所有计算逻辑均依托 Python 标准库的 fractions.Fraction 模块进行绝对精度的有理数运算,从根本上杜绝了浮点数丢失精度的问题。生成的文本格式经过正则表达式严格清洗,保证了判断算法的准确性。
6. 项目小结
本次项目出于个人实际情况,我采取了单人全栈完成的模式。这为我带来了与平时简单编码练习完全不同的工程化体会:
- 成败得失与经验: 最大的成功在于没有采用“随机拼字符串”这种投机取巧且难以维护的办法,而是彻底搞懂并应用了抽象语法树(AST)。利用 AST,后续无论是计算节点值、加减括号、还是特征序列化去重,逻辑都变得异常清晰,程序的健壮性得到了极大的提升。
- 总结教训: 最初我在实现
-e判卷功能时,试图手写一个分数堆栈计算器,结果因为诸如2'1/2这种特殊的带分数格式频频报错。痛定思痛后,我转向利用正则表达式将特殊算式转换回 Python 原生可识别的Fraction语句再进行eval(),不仅去除了数百行冗余代码,还保证了零 Bug。这让我深刻认识到:在工程实践中,善用正则和原生高级特性比重复造轮子重要得多。 - 感悟: 尽管缺乏“结对编程”中领航员的实时纠错,但这也逼迫我自己去完善边缘测试用例,并熟练掌握了
cProfile等效能分析工具。这种独立解决系统级问题的能力,对我日后参与更复杂的软件工程大有裨益。
浙公网安备 33010602011771号