结对项目

结对项目:自动生成小学四则运算题目
姓名:陈凯壕 学号:3124004051
GitHub 项目地址:https://github.com/mosquito512/double

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

二、效能分析
2.1 性能测试结果
使用 Python cProfile 对生成 10000 道题目(-r 20)进行性能分析:
10000 problems generated in ~0.74 seconds
2,335,504 function calls in 0.739 seconds
各函数累计耗时(Top 5):
函数 调用次数 累计耗时(秒) 占比
generate_problem 11,726 0.690 93.4%
gen_expression 57,298 0.371 50.2%
gen_number 37,204 0.168 22.7%
frac_to_str 75,058 0.092 12.4%
evaluate 29,754 0.087 11.8%
2.2 性能优化思路
1.使用 Python 内置 fractions.Fraction:避免浮点数精度问题,所有分数运算自动约分,无需手动处理通分。
2.哈希集合去重:使用 set 存储规范化表达式字符串,O (1) 查找,相比列表 in 操作(O (n))大幅提升去重效率。
3.递归预算分配:生成表达式时,将运算符预算在递归层间分配(left_ops + right_ops = remaining),避免深层递归。
4.惰性求值:在生成过程中仅对需要判断约束的子表达式求值,不重复计算。
2.3 消耗最大的函数
generate_problem 是消耗最大的函数(累计 0.690s),其中主要时间花在递归调用 gen_expression 上。由于需要反复生成并校验约束(非负、真分数加法等),部分表达式需要重试。通过设置合理的重试上限(200 次 / 层)和提前终止概率(25%),在保证题目多样性的同时控制了重试开销。

三、设计实现过程
3.1 代码组织
项目采用面向对象设计,核心分为三个模块:
main.py

├── 分数工具函数(frac_to_str, str_to_frac)

├── 表达式 AST

│ ├── Number 类(数字叶子节点)

│ └── BinOp 类(二元运算节点)

├── 表达式生成器

│ ├── gen_number() — 生成自然数/真分数

│ ├── gen_expression() — 递归生成表达式树

│ ├── count_ops() — 统计运算符数量

│ └── generate_problem() — 生成一道完整题目

├── 生成模式 run_generate()

├── 判题模式 run_grade()

└── 命令行入口 main()

3.2 关键设计决策
(1)表达式树结构
使用 AST(抽象语法树)表示算术表达式,每个节点有 evaluate() 和 normalize() 两个核心方法:
evaluate():递归计算表达式的值,返回 Fraction 类型,保证精确运算。
normalize():返回表达式的规范化字符串,用于去重判断。
(2)括号插入规则
为了让题目显示与实际求值一致,BinOp.str 根据运算符优先级和结合性决定是否给子表达式加括号:
左子节点:子表达式优先级更低时加括号(如 (1+2)×3);同优先级不加(左结合)。
右子节点:子表达式优先级更低时加括号;同优先级时,- 和 ÷ 需加括号(右结合性不同),+ 和 × 不加(可结合)。
(3)去重算法
对 + 和 × 运算符,将左右子树的规范化字符串排序后拼接,使得 23+45 和 45+23 产生相同的去重键:
if self.op in ('+', '×'):
lstr, rstr = sorted([lstr, rstr])
return f"({lstr}{self.op}{rstr})"
(4)约束满足策略
减法非负:生成左右子表达式后,若 left < right 则交换两者。
真分数加法:若左右操作数都 < 1 且和 ≥ 1,则重新生成。
除法非零:除数为 0 时重新生成。

四、代码说明
4.1 分数类(Fraction 封装)
使用 Python 内置 fractions.Fraction 进行精确分数运算,显示时转换为题目要求的格式:
def frac_to_str(f: PyFraction) -> str:
if f.denominator == 1:
return str(f.numerator)

假分数 → 带分数 a'b/c

if abs(f.numerator) > f.denominator:
integer_part = f.numerator // f.denominator
remainder = f.numerator % f.denominator
return f"{integer_part}'{remainder}/{f.denominator}"
return f"{f.numerator}/{f.denominator}"

4.2 表达式生成(递归)
def gen_expression(r: int, max_ops: int):
if max_ops <= 0:
return gen_number(r)
if random.random() < 0.25: # 提前终止概率
return gen_number(r)
op = random.choice(['+', '-', '×', '÷'])
remaining = max_ops - 1
left_ops = random.randint(0, remaining)
right_ops = remaining - left_ops
for _ in range(200):
left = gen_expression(r, left_ops)
right = gen_expression(r, right_ops)
lv, rv = left.evaluate(), right.evaluate()
valid = True
if op == '-' and lv < rv:
left, right = right, left # 交换保证非负
elif op == '+' and lv < 1 and rv < 1 and lv + rv >= 1:
valid = False # 真分数加法结果须为真分数
elif op == '÷' and rv == 0:
valid = False # 除数不为0
if valid:
return BinOp(op, left, right)
return gen_number(r)

4.3 判题逻辑
def evaluate_expression(expr_str: str) -> PyFraction:
s = expr_str.replace("×", "*").replace("÷", "/")
s = re.sub(r"(\d+)'(\d+)/(\d+)", r"(\1+\2/\3)", s) # 带分数
s = re.sub(r"(\d+)/(\d+)", r"Fraction(\1,\2)", s) # 真分数
result = eval(s, {"__builtins__": {}}, {"Fraction": PyFraction})
...

五、测试运行
测试用例
编号 测试内容 命令 预期结果 实际结果
1
基本生成(10 题,范围 10)
python main.py -n 10 -r 10
生成 10 道题,写入 Exercises.txt 和 Answers.txt
✅ 通过
2
大量生成(10000 题)
python main.py -n 10000 -r 20
0.7 秒内生成 10000 道不重复题目
✅ 通过
3
缺少 - r 参数
python main.py -n 10
报错并显示帮助信息
✅ 通过
4
判题模式(全对)
python main.py -e Exercises.txt -a Answers.txt
Correct: 10000, Wrong: 0
✅ 通过
5
判题模式(有错)
修改部分答案后判题
正确列出对 / 错题号
✅ 通过
6
减法非负验证
检查所有减法子表达式
无负数结果
✅ 通过
7
运算符数量验证
统计每题运算符数
均不超过 3 个
✅ 通过
8
题目去重验证
生成 10000 题后检查唯一性
无重复题目
✅ 通过
9
真分数格式验证
检查带分数输出格式
2'3/8 格式正确
✅ 通过
10
括号插入验证
检查含混合优先级的题目
括号正确,求值一致
✅ 通过
11
小范围生成
python main.py -n 5 -r 2
正常生成自然数题目
✅ 通过
12
除法零除数防护
大量生成检查
无除以 0 的错误
✅ 通过

正确性保证
精确运算:所有计算使用 fractions.Fraction,不存在浮点精度误差。
交叉验证:生成答案后,用判题模式重新求值全部 10000 道题目,与预存答案逐一比对,0 错误。
约束穷举测试:通过大量生成检查所有约束条件(非负、真分数加法、运算符数量)均被满足。
去重验证:规范化字符串集合保证任意两道题目无法通过交换 +/× 操作数变为同一题。

六、项目小结
6.1 成功之处
精确分数运算:选用 Python fractions.Fraction 避免了浮点误差,所有题目答案精确无误。
去重算法:通过 AST 规范化 + 集合哈希,高效实现了题目去重,10000 题仅需 0.7 秒。
括号自动插入:根据优先级和结合性自动决定括号,保证题目显示与数学求值一致。
6.2 不足之处
真分数加法约束可能导致部分表达式生成失败重试,在较小范围下题目多样性受限。
判题模块使用 eval 解析表达式,虽然做了沙箱隔离,但安全性不如完整的递归下降解析器。

附录:运行说明
# 生成 100 道 20 以内的四则运算题目
python main.py -n 100 -r 20
# 判题:对比题目文件和答案文件
python main.py -e Exercises.txt -a Answers.txt
# 输出文件:
# Exercises.txt — 题目列表
# Answers.txt — 标准答案
# Grade.txt — 判题结果(Correct/Wrong)

posted @ 2026-09-21 19:26  fb-ghj  阅读(12)  评论(0)    收藏  举报