结对项目 —— 小学四则运算题目生成器

姓名:谭铭昊 学号:3124004068

姓名:周子骞 学号:3124004077

GitHub 项目地址https://github.com/RicharTan-zlz/pair-programming-homework


这个作业属于哪个班级 https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS
这个作业要求在哪里 https://edu.cnblogs.com/campus/gdgy/Class56-Grade2024-CS/homework/15694
这个作业的目标 结对完成题目为自动生成小学四则运算题目的图像界面程序。

一、PSP 表格

1.1 预估耗时

PSP2.1 Personal Software Process Stages 预估耗时(分钟)
Planning 计划
· Estimate · 估计这个任务需要多少时间 30
Development 开发
· Analysis · 需求分析(包括学习新技术) 40
· Design Spec · 生成设计文档 30
· Design Review · 设计复审 20
· Coding Standard · 代码规范 15
· Design · 具体设计 40
· Coding · 具体编码 180
· Code Review · 代码复审 30
· Test · 测试(自我测试,修改代码,提交修改) 90
Reporting 报告
· Test Report · 测试报告 30
· Size Measurement · 计算工作量 15
· Postmortem & Process Improvement Plan · 事后总结,提出过程改进计划 30
合计 550

1.2 实际耗时

PSP2.1 Personal Software Process Stages 实际耗时(分钟)
Planning 计划
· Estimate · 估计这个任务需要多少时间 25
Development 开发
· Analysis · 需求分析 60
· Design Spec · 生成设计文档 40
· Design Review · 设计复审 25
· Coding Standard · 代码规范 20
· Design · 具体设计 50
· Coding · 具体编码 240
· Code Review · 代码复审 40
· Test · 测试 120
Reporting 报告
· Test Report · 测试报告 35
· Size Measurement · 计算工作量 15
· Postmortem & Process Improvement Plan · 事后总结 30
合计 700

分析:实际耗时 700 分钟 > 预估 550 分钟。主要超支在编码(+60)和测试(+30),原因是表达式树去重算法和批改模式中"分数/除法混淆"bug 的排查耗费较多时间。后续应预留更多 buffer。


二、设计实现过程

2.1 模块结构

程序分为 5 层:

┌──────────────────────────────────┐
│  main()  命令行入口(argparse)  │
├──────────────────────────────────┤
│  生成模式 / 批改模式 两种分支     │
├──────────────────────────────────┤
│  表达式树(tree_*)              │
├──────────────────────────────────┤
│  分数工具(format/parse/eval)   │
├──────────────────────────────────┤
│  随机数生成(gen_number)        │
└──────────────────────────────────┘

核心函数

函数 功能
format_fraction(f) Fraction → 字符串(3/5, 2'3/8
parse_fraction(s) 字符串 → Fraction
eval_fraction_expr(expr) 精确计算表达式(批改模式用)
gen_number(r) 生成 [0, r) 内的自然数或真分数
gen_tree(r, max_ops) 递归生成表达式树
tree_value(node) 递归求值
tree_to_string(node) 表达式树 → 字符串(按需加括号)
tree_normalize(node) 表达式树规范化(去重用)
generate(n, r) 生成 n 道不重复题目
grade(ex_path, ans_path) 批改模式

2.2 关键算法:表达式树

二叉树表示表达式:

  • 数字节点('num', Fraction)
  • 运算节点('op', op_char, left, right)

生成流程图

        [开始生成]
           │
           ▼
   ┌──────────────┐
   │ 随机选运算符 │
   └───────┬──────┘
           │
     ┌─────┴─────┐
     ▼           ▼
  递归左子     递归右子
     │           │
     └─────┬─────┘
           ▼
   ┌──────────────┐
   │ 求值 + 验证   │
   │ · 减法≥0     │
   │ · 除法<1     │
   └───────┬──────┘
           │
           ▼
     加入题库

求值天然支持数学优先级:递归计算时先算子节点再算父节点,等价于"先乘除后加减"。

打印时按需加括号

  • 左子优先级 < 父优先级 → 加括号:(1 + 2) × 3
  • 右子优先级 < 父优先级,或同级且父为 -/÷ → 加括号:1 - (2 - 3)

2.3 关键算法:去重

作业要求:23 + 4545 + 23 视为重复,3 + (2 + 1)1 + 2 + 3 视为重复。

实现tree_normalize 递归处理:

  • 加法:左右子树排序后拼接
  • 乘法:左右子树排序后拼接
  • 减法、除法:不排序(不满足交换律)

示例

# 1 + 2 与 2 + 1 归一化后相同
normalize(1+2) == normalize(2+1)  # True

# 2 - 1 与 1 - 2 不同
normalize(2-1) != normalize(1-2)  # True

2.4 关键算法:批改模式

难点1/2 ÷ 3/4 中,/ 既表示分数又表示除法,必须区分。

解决方案

  1. ÷ 替换为特殊符号 //
  2. a/b 形式的分数替换为 Fraction(a, b)
  3. 把剩余的纯数字替换为 Fraction(n)
  4. 最后把 // 替换回 /(Python 中 Fraction 的 / 就是真除法)

效果

eval("1/2 ÷ 3/4")   # 结果 Fraction(2, 3)
eval("1/2 ÷ 4 ÷ 5") # 结果 Fraction(1, 40)

2.5 核心代码

def gen_tree(r: int, max_ops: int, depth: int = 0):
    """递归生成表达式树,返回 (node, None) 或 (None, None)"""
    if max_ops == 0 or (depth > 0 and random.random() < 0.35):
        return gen_number_node(r), None

    op = random.choice(['+', '-', '×', '÷'])
    left_ops = random.randint(0, max_ops - 1)
    right_ops = max_ops - 1 - left_ops

    left_node, _ = gen_tree(r, left_ops, depth + 1)
    right_node, _ = gen_tree(r, right_ops, depth + 1)

    lv = tree_value(left_node)
    rv = tree_value(right_node)

    if op == '-':
        if lv < rv:  # 交换左右,保证不产生负数
            left_node, right_node = right_node, left_node
            lv, rv = rv, lv
    elif op == '÷':
        if rv == 0 or lv / rv >= 1:  # 保证除法的结果是真分数
            return None, None

    node = ('op', op, left_node, right_node)
    return node, None

三、性能分析

3.1 性能分析图

使用 cProfile + snakeviz 分析 10000 道题生成:

performance

3.2 数据分析

测试条件:10000 道题目生成,总耗时 1.046 秒

函数 耗时占比 说明
generate() 90% 主生成流程
gen_tree() 50% 表达式树递归生成(瓶颈)
gen_number() 20% 随机数生成
tree_value() 11% 表达式求值
format_fraction() 10% 分数格式化
tree_to_string() 9% 表达式转字符串

3.3 瓶颈分析

  • gen_tree 调用 106766 次,但只有 23502 次成功(成功率 22%)
  • 78% 的失败来自约束检查(负数、除法 ≥ 1、无意义题目)

3.4 优化思路

理论上可优化

  • 用"预生成候选数 + 约束筛选"替代递归重试
  • 缓存随机数,减少 random.randint 调用

实际上不需要:1 秒生成 10000 题,远低于 5 秒要求,优化收益不大。


四、单元测试展示

4.1 测试策略

  • 白盒:覆盖所有分支(空输入、减法负数、除法 ≥ 1、运算符超限)
  • 黑盒:等价类划分(正常、边界、异常)
  • 共设计 30 个测试用例

4.2 测试模块分布

模块 用例数 覆盖内容
TestFractionUtils 7 分数格式化 / 解析
TestEvalExpression 6 表达式精确求值
TestExpressionTree 7 树求值 / 打印 / 去重
TestGeneration 6 生成器约束验证
TestIO 4 文件读写 / 批改模式

4.3 测试代码片段

class TestEvalExpression(unittest.TestCase):
    """表达式精确求值测试"""

    def test_priority_mul_before_add(self):
        """测试9: 先乘除后加减 - 1 + 2 × 3 = 7"""
        self.assertEqual(eval_fraction_expr("1 + 2 × 3"), Fraction(7))

    def test_priority_div_before_sub(self):
        """测试10: 1/3 - 1/6 ÷ 3 = 5/18"""
        self.assertEqual(eval_fraction_expr("1/3 - 1/6 ÷ 3"), Fraction(5, 18))

    def test_fraction_division(self):
        """测试13: 1/2 ÷ 3/4 = 2/3"""
        self.assertEqual(eval_fraction_expr("1/2 ÷ 3/4"), Fraction(2, 3))


class TestIO(unittest.TestCase):
    """文件 IO 测试"""

    def test_grade_correct(self):
        """测试29: 批改模式 - 全对"""
        with tempfile.TemporaryDirectory() as d:
            ex = os.path.join(d, "ex.txt")
            ans = os.path.join(d, "ans.txt")
            with open(ex, 'w', encoding='utf-8') as f:
                f.write("1. 1 + 2 =\n2. 3 × 4 =\n")
            with open(ans, 'w', encoding='utf-8') as f:
                f.write("1. 3\n2. 12\n")
            old_cwd = os.getcwd()
            os.chdir(d)
            try:
                grade(ex, ans)
                with open("Grade.txt", encoding='utf-8') as f:
                    content = f.read()
                self.assertIn("Correct: 2", content)
                self.assertIn("Wrong: 0", content)
            finally:
                os.chdir(old_cwd)

4.4 测试结果

unittest

Ran 30 tests in 0.037s

OK

4.5 正确性验证

100 题批改测试

grade

10000 题压力测试

  • 生成时间:约 1 秒
  • 批改结果:Wrong: 0,100% 正确

4.6 测试正确性说明

为什么能确定程序正确

  1. 白盒测试:覆盖所有分支(空输入、减法负数、除法 ≥ 1、运算符超限)
  2. 边界测试-r 2-n 1、极值分数
  3. 端到端测试:生成 → 批改 → 对比原答案
  4. 交叉验证:批改模式独立计算答案,与生成模式的答案对比,10000 题全部一致

五、代码质量

5.1 pylint 评分

pylint

5.2 代码规范

  • 所有函数均有 docstring
  • 变量命名使用 snake_case
  • 常量使用 UPPER_CASE
  • 无未使用变量 / 导入
  • 无过长函数(均 < 50 行)

六、异常处理说明

虽然本项目未引入自定义异常类,但通过流程约束保证鲁棒性:

场景 处理方式
命令行参数缺失 argparse 报错 + 使用说明
-r 参数 ≤ 0 print("错误: -r 必须为正整数") + exit(1)
文件不存在 openFileNotFoundError
表达式求值失败 try/except 跳过该题
无法生成足够题目 打印警告并生成实际数量

七、Git 提交记录

5 次有意义的 commit,符合"每完成一个功能就提交"的要求:

# commit 内容
1 fba957b Initial commit
2 c7da74e feat: 实现四则运算表达式树生成器核心
3 e6e9752 test: add 30 unit tests for core modules
4 e628c61 chore: add requirements and gitignore
5 115af8f docs: add README

八、运行方式

# 无需第三方依赖,仅需 Python 3.8+
python main.py -n 10 -r 10

# 批改模式
python main.py -e Exercises.txt -a Answers.txt

# 运行单元测试
python -m unittest test_main.py

九、项目小结

9.1 结对分工

  • 谭铭昊:主笔编码,负责表达式树生成、批改模式、单元测试
  • 周子骞:协助设计、代码 review、测试数据准备

9.2 遇到的困难

最大难题:批改模式的"分数/除法混淆"

批改时,1/2 ÷ 3/4 这个表达式中,/ 既表示分数又表示除法。最初用简单的 re.sub 替换,导致:

  • 1/2 ÷ 4 ÷ 5 被算成 1/2 ÷ (4/5) = 5/8(错)
  • 正确答案应为 1/2 ÷ 4 ÷ 5 = 1/40

解决:用 // 临时符号区分"除法运算符",先替换分数,再还原 //

这个 bug 花了 2 小时排查,最后通过对比生成模式答案 vs 批改模式答案定位。

9.3 经验教训

  1. 测试先行:如果一开始就写"1/2 ÷ 4 ÷ 5"的测试用例,可以提早发现 bug
  2. 表达式解析很坑:优先考虑用 AST 或者手工词法分析,而不是正则
  3. 性能分析能定位真实瓶颈:cProfile 让我们知道 gen_tree 占 50%,而不是想象中的分数运算

9.4 结对感受

闪光点

  • 两人同时 review 表达式树的去重逻辑,互相 catch 到了对方没考虑到的边界(1+2+3 vs 3+2+1
  • 遇到 bug 时一起分析,比单打独斗快很多

建议

  • 下次应该先写出所有测试用例,再写代码
  • 应该提早 commit,不要等所有功能做完才提交

十、总结

本次结对项目完整实现了作业的 9 项需求(1-9),从生成、约束、去重、IO 到批改模式全部通过 10000 题测试。

核心成果

  • 10000 题生成 + 批改,100% 正确,耗时 1 秒
  • 单元测试 30/30 通过
  • pylint 10.00/10
  • 5 次有意义的 Git commit

未实现:附加分需求 10(查重功能),因时间有限未做。


posted @ 2026-09-20 20:32  RICHAR212  阅读(7)  评论(0)    收藏  举报