treec34

导航

 
这个作业属于哪个课程 【计科6】
这个作业要求在哪里 个人项目:自动生成小学四则运算题目
这个作业的目标 独立完成命令行运算程序的开发全流程,掌握 AST(抽象语法树)设计、性能分析(cProfile+SnakeViz)、自动化测试及项目文档编写。

作者信息:


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)。

性能分析图展示:
image

改进思路与分析:
从性能图谱中可以清晰地看到,程序消耗最大的函数是 generate_ast(抽象语法树生成逻辑)以及底层的 generate_random_fraction(随机分数生成)。
在初期版本中,由于存在大量不合规的除法节点(除数为0或结果大于1)以及产生负数的减法节点,导致树的递归生成频繁触发 continue 重试。

  • 改进方案:针对减法可能产生负数的问题,我取消了粗暴的丢弃重试,改为直接交换左右子节点 (left, right = right, left),极大地减少了无效递归。针对去重,设计了 get_canonical 特征序列化函数,利用 Set 的 $O(1)$ 哈希查询代替了对树结构的深层遍历比对,确保了 1 万道题在 1 秒左右即可生成完毕。

3. 设计实现过程

本项目采用面向对象编程与抽象语法树(AST)的架构体系。核心设计如下:

  1. 类结构: 定义了 Node 类,代表 AST 中的每一个节点。非叶子节点存储 op(运算符)、left 和 right,叶子节点直接存储 value(基于 fractions.Fraction 实例)。
  2. 核心函数拆解:
    • 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 转换为 Python eval() 可计算的字符串表达式进行比对。
  3. 流程关系: 命令行 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。

image

正确性说明:
所有计算逻辑均依托 Python 标准库的 fractions.Fraction 模块进行绝对精度的有理数运算,从根本上杜绝了浮点数丢失精度的问题。生成的文本格式经过正则表达式严格清洗,保证了判断算法的准确性。


6. 项目小结

本次项目出于个人实际情况,我采取了单人全栈完成的模式。这为我带来了与平时简单编码练习完全不同的工程化体会:

  • 成败得失与经验: 最大的成功在于没有采用“随机拼字符串”这种投机取巧且难以维护的办法,而是彻底搞懂并应用了抽象语法树(AST)。利用 AST,后续无论是计算节点值、加减括号、还是特征序列化去重,逻辑都变得异常清晰,程序的健壮性得到了极大的提升。
  • 总结教训: 最初我在实现 -e 判卷功能时,试图手写一个分数堆栈计算器,结果因为诸如 2'1/2 这种特殊的带分数格式频频报错。痛定思痛后,我转向利用正则表达式将特殊算式转换回 Python 原生可识别的 Fraction 语句再进行 eval(),不仅去除了数百行冗余代码,还保证了零 Bug。这让我深刻认识到:在工程实践中,善用正则和原生高级特性比重复造轮子重要得多。
  • 感悟: 尽管缺乏“结对编程”中领航员的实时纠错,但这也逼迫我自己去完善边缘测试用例,并熟练掌握了 cProfile 等效能分析工具。这种独立解决系统级问题的能力,对我日后参与更复杂的软件工程大有裨益。
posted on 2026-09-22 15:33  treec34  阅读(14)  评论(0)    收藏  举报