水每石帝

第三周作业:结对项目

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

GitHub仓库https://github.com/shiningbrightlydarkmoon/shiningbrightlydarkmoon/tree/main/MathExercise

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

一、项目简介

本项目实现了一个小学四则运算题目生成和批改程序。程序使用 Python 编写,完全使用精确分数计算,不依赖浮点数。它支持按指定范围生成题目、限制运算符数量和中间结果、按题目结构去重,以及根据题目文件和答案文件自动判分。

主要功能如下:

  • 通过 -n 指定生成题目的数量。
  • 通过 -r 指定题目和分数分母的取值范围。
  • 每道题最多包含 3 个运算符。
  • 减法不会产生负数。
  • 除法结果必须是正的真分数。
  • 题目不能通过交换 +× 左右子树变成重复题。
  • 生成结果写入 Exercises.txt
  • 使用 -e-a 对题目和答案文件进行判分。
  • 判分结果写入 Grade.txt

关键规则:

  1. 减法 e1 - e2 要求 e1 >= e2,不能出现负数。
  2. 除法 e1 ÷ e2 的除数不能为 0,结果必须是正的真分数。
  3. 两道题如果可以通过交换 +× 的左右子树互相转换,则视为重复题。
  4. 2+33+2 重复,1+2+33+(2+1) 重复。
  5. 1+2+33+2+1 不重复,因为两者表达式树结构不同。

二、PSP2.1 表格

下表中的预估时间可以按实际开发情况调整。实际时间必须在开发、测试和博客完成后据实填写。

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

三、设计实现过程

3.1 总体结构

程序采用“命令行层、业务模块、领域模型”的结构:

main.py
  └── src/cli.py
        ├── src/generator.py
        │     └── src/expression.py
        │           └── src/rational.py
        ├── src/exercise_io.py
        │     └── src/parser.py
        └── src/grader.py
              └── src/expression.py
                    └── src/rational.py

3.2 模块说明

模块 主要职责
rational.py 精确分数运算、约分、格式化和解析
expression.py 表达式树、求值、括号渲染和去重标识
parser.py 解析题目文件中的表达式
generator.py 随机生成合法题目并进行去重
exercise_io.py 读写题目文件和答案文件
grader.py 计算标准答案、比较答案、输出判分统计
cli.py 解析 -n-r-e-a 参数并调用相应流程

3.3 关键流程

生成题目的流程:

读取 -n 和 -r
  -> 随机生成 1 到 3 个运算符的表达式树
  -> 递归检查减法和除法约束
  -> 计算规范化标识
  -> 判断是否重复
  -> 题目数量达到要求后写入 Exercises.txt

判分的流程:

读取 Exercises.txt
  -> 解析每道题的表达式
  -> 读取 Answers.txt
  -> 使用表达式树计算标准答案
  -> 解析并比较用户答案
  -> 统计正确和错误题号
  -> 写入 Grade.txt

四、性能分析

4.1 测试环境

  • Python 版本:3.14.7
  • 操作系统:Windows
  • 测试范围:r=10
  • 测试内容:题目生成、约束检查和去重,不包含写文件时间

运行命令:

python tools/benchmark.py --r 10 --counts 1000,5000,10000 --profile-count 10000

4.2 性能数据

题目数量 生成耗时(秒) 去重后数量
1000 0.024164 1000
5000 0.120939 5000
10000 0.255370 10000

从结果可以看出,题目数量从 1000 增加到 10000 时,耗时接近线性增长。10000 道题在不到 1 秒的时间内完成生成和去重。

image

4.3 最大耗时函数

使用 cProfile 分析 10000 道题后,累积耗时最高的函数是:

src/generator.py: _build_random_expression

主要热点函数统计如下:

函数 调用次数 累积耗时
generate 1 1.031 秒
_build_random_expression 102708/20506 0.622 秒
_random_leaf 61607 0.347 秒
evaluate 93669/18908 0.232 秒
canonical_key 102708/20506 0.111 秒

cProfile 本身会带来额外开销,因此这里的总时间和正常一键运行的时间不同。正常运行时,10000 道题生成约需 0.255 秒。

7f4b9ed365871dbbc03e0948834e42dd

4.4 性能优化思路

  1. 使用 canonical_key 集合去重,查找平均为 O(1),没有使用题目之间的两两比较。
  2. 每道题最多 3 个运算符,表达式树最多 7 个节点,递归深度固定。
  3. 所有计算使用 Fraction,避免浮点数误差和重复转换。
  4. r 很小时采用枚举方式,避免随机重试导致长时间循环。
  5. 在内存中完成生成后一次性写入文件,减少文件 I/O 次数。

五、关键代码说明

5.1 精确分数

Rational 内部使用 fractions.Fraction,所有加减乘除都由精确分数完成。

@dataclass(frozen=True)
class Rational:
    value: Fraction

    @classmethod
    def from_parts(cls, numerator: int, denominator: int = 1) -> "Rational":
        if denominator == 0:
            raise RationalError("分母不能为 0")
        return cls(Fraction(numerator, denominator))

    def format(self) -> str:
        numerator = self.numerator
        denominator = self.denominator
        sign = "-" if numerator < 0 else ""
        absolute = abs(numerator)
        whole, remainder = divmod(absolute, denominator)
        if remainder == 0:
            return f"{sign}{whole}"
        if whole:
            return f"{sign}{whole}’{remainder}/{denominator}"
        return f"{sign}{absolute}/{denominator}"

这样既能支持 3/5,也能支持 2’3/8

5.2 表达式求值

每个运算节点都会递归计算左右子树,并在减法、除法时检查题目约束。

def evaluate(self) -> Rational:
    if self.is_leaf:
        return self.value

    left = self.left.evaluate()
    right = self.right.evaluate()

    if self.operator == "+":
        return left + right
    if self.operator == "-":
        if left < right:
            raise ExpressionError("减法产生了负数")
        return left - right
    if self.operator == "×":
        return left * right
    if self.operator == "÷":
        if right.is_zero():
            raise ExpressionError("除数不能为 0")
        result = left / right
        if not result.is_proper():
            raise ExpressionError("除法结果必须为正的真分数")
        return result

只要任意子表达式不满足规则,整道题就不会被生成。

5.3 去重标识

去重不使用字符串直接比较,也不把整棵表达式树全部摊平,而是在每个 +× 节点对子树标识排序。

def canonical_key(self) -> str:
    if self.is_leaf:
        return f"N{self.value.numerator}/{self.value.denominator}"

    left_key = self.left.canonical_key()
    right_key = self.right.canonical_key()

    if self.operator in {"+", "×"}:
        left_key, right_key = sorted((left_key, right_key))

    return f"({self.operator}{left_key},{right_key})"

这可以保证:

2 + 3                == 3 + 2
2 × 3                == 3 × 2
1 + 2 + 3            == 3 + (2 + 1)
1 + 2 + 3            != 3 + 2 + 1

5.4 判分实现

判分模块复用表达式求值逻辑,避免“生成时一种算法、判分时另一种算法”导致结果不一致。

expected = item.expression.evaluate()
actual = Rational.parse(raw_answer)
is_correct = actual == expected

比较结果后分别收集正确题号和错误题号,最后生成规定格式:

Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)

六、测试运行

6.1 自动化测试

运行命令:

python -m unittest discover -s tests -v

实际结果:

Ran 17 tests in 0.395s
OK

4e8410522d450dc588fae366200e1a50

6.2 测试用例

编号 测试内容 输入或操作 预期结果 实际结果
1 分数解析 3/5 输出 3/5 通过
2 带分数解析 2’3/8 转换为 19/8 通过
3 分数加法 1/6 + 1/8 7/24 通过
4 交换律去重 2+33+2 规范化标识相同 通过
5 结合与交换去重 1+2+33+(2+1) 规范化标识相同 通过
6 不同树结构 1+2+33+2+1 规范化标识不同 通过
7 渲染回读 生成表达式渲染后再解析 规范化标识不变 通过
8 减法约束 1-2 抛出“减法产生负数”错误 通过
9 除法约束 1÷2 结果为 1/2 通过
10 非法除法 2÷1 判定为非法题目 通过
11 1000 道题生成 ProblemGenerator(10).generate(1000) 1000 道题且不重复 通过
12 小范围空间不足 r=1,n=10000 报告无法生成足够题目 通过
13 编号答案判分 1. 7/24 题号统计正确 通过
14 无编号答案判分 1/2 按顺序判分 通过
15 缺少 -r -n 5 输出帮助信息并非零退出 通过
16 端到端生成与判分 -n 12 -r 10 后判分 Correct: 12 通过

0b2edb782835723ca6049a4cf6af11b4

6.3 为什么可以判断程序正确

  1. 所有计算都使用精确分数,不依赖浮点数。
  2. 每个表达式都会递归检查所有子表达式,非法中间结果不会进入题目文件。
  3. 去重同时覆盖 +× 的交换律,同时保留不同树结构之间的区别。
  4. 生成结果会再次被解析,验证输出格式和表达式树可以正常回读。
  5. 判分模块复用题目求值模块,降低两套算法结果不一致的风险。
  6. 自动化测试覆盖了分数、表达式、生成器、判分和命令行。
  7. 10000 道题测试同时检查了数量、唯一性和最大运算符数量。

七、运行与示例

7.1 生成题目

python main.py -n 10 -r 10

07a4997517e27df73eaa9a6a347d3781

生成的 Exercises.txt 示例:

1. 7 - 1’1/2 - 4/7 + 3/4 =
2. 3 ÷ (6’1/4 + 5) =
3. 7/8 × 0 =
4. 6 × (7’4/9 - 5) =
5. 2/3 + 4/5 =

image

7.2 生成 10000 道题

python main.py -n 10000 -r 10
python tools/validate.py Exercises.txt --r 10

校验结果:

校验通过: exercises=10000 unique=10000 max_ops=3

3d267f707d7682bfdc7e00a89cdd68fb

7.3 批改答案

python main.py -e Exercises.txt -a Answers.txt

d1a2e5a45dd61caf8bd37302d5f1f991

Grade.txt 示例:

Correct: 7 (1, 2, 3, 4, 5, 6, 8)
Wrong: 3 (7, 9, 10)

image

7.4 缺少 -r 时的帮助信息

python main.py -n 5

程序会提示必须提供 -r,并打印帮助信息。

25e4cc3756901691bcac3d8bd876d53a

八、项目小结

本项目最重要的两个部分是精确分数运算和题目去重。分数运算如果使用浮点数,容易出现答案比较不一致的问题;因此我们使用 Fraction 保存精确值,并在输出时转换成普通分数或带分数。

去重没有采用简单的字符串比较。我们为每个表达式树生成规范化标识,只在 +× 节点交换左右子树,不把整棵表达式树直接摊平。这样既能识别 2+33+2,也能正确处理题目中特别说明的 1+2+33+(2+1)3+2+1

程序还处理了小范围 r 的边界情况。当 r=1 或合法题目空间不足时,程序不会无限尝试,而是输出明确错误,方便用户调整 -r-n

九、总结

本次结对项目完成了题目生成、去重、文件输出、答案判分、自动化测试和性能分析。我们将复杂规则拆分成可单独测试的模块,并使用精确分数避免计算误差。最终程序可以生成 10000 道互不重复的合法题目,能够正确完成判分,并给出了明确的运行说明和测试记录。

通过这次合作,我们认识到结对项目不只是把代码分成两半,还需要共同确认接口、交叉测试、检查提交记录,并把实现过程中的设计思路、问题解决方法和经验总结写进博客。后续如果继续扩展,可以考虑增加图形界面、支持更多运算符,或者生成除题目之外的提示和解析步骤。

PSP2.1 Personal Software Process Stages 实际耗时(分钟)
Planning 计划 15
Estimate 估计这个任务需要多少时间 15
Development 开发
Analysis 需求分析(包括学习新技术) 55
Design Spec 生成设计文档 60
Design Review 设计复审(和同事审核设计文档) 25
Coding Standard 代码规范(为目前的开发制定合适的规范) 25
Design 具体设计 65
Coding 具体编码 450
Code Review 代码复审 70
Test 测试(自我测试,修改代码,提交修改) 180
Reporting 报告
Test Report 测试报告 55
Size Measurement 计算工作量 15
Postmortem & Process Improvement Plan 事后总结,并提出过程改进计划 65
合计 1095

GitHub仓库截图
d7ada73239434243ea7063c11da09e93

posted on 2026-09-20 18:00  水每石帝  阅读(13)  评论(0)    收藏  举报

导航