结对项目
Github地址:https://github.com/Philzair/Philzair/tree/main/thirdweek
结对项目:一个自动生成小学四则运算题目的命令行程序
| 这个作业属于哪个课程 | 计科56班 |
|---|---|
| 这个作业要求在哪里 | 结对项目 |
| 这个作业的目标 | 实现一个自动生成小学四则运算题目的命令行程序 |
-
姓名、学号:
成员1:陈宇轩 3124004089
成员2:李家乐 3124004096
一、PSP2.1
我们在开始编码前对耗时进行了预估,实际耗时根据需求分析、设计、编码、测试和报告工作的完成情况统计。
| PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|---|
| Planning | 计划 | 30 | 35 |
| Estimate | 估计任务时间 | 30 | 35 |
| Development | 开发 | 480 | 510 |
| Analysis | 需求分析(包括学习新技术) | 50 | 60 |
| Design Spec | 生成设计文档 | 45 | 50 |
| Design Review | 设计复审 | 25 | 30 |
| Coding Standard | 制定代码规范 | 20 | 20 |
| Design | 具体设计 | 55 | 60 |
| Coding | 具体编码 | 160 | 170 |
| Code Review | 代码复审 | 35 | 40 |
| Test | 测试、修改、提交 | 90 | 80 |
| Reporting | 报告 | 90 | 110 |
| Test Report | 测试报告 | 40 | 45 |
| Size Measurement | 计算工作量 | 15 | 20 |
| Postmortem | 事后总结与改进计划 | 35 | 45 |
| 合计 | 600 | 655 |
二、设计与实现过程
程序分为三层:Myapp.py 负责命令行和文件;ExerciseGenerator 负责生成;Expression、解析器
及批改函数负责领域逻辑。表达式采用不可变二叉树,每个节点同时保存精确的 Fraction 值。
程序设计流程图

生成一个运算节点时先递归生成左右子树。减法会保证左值不小于右值;除法会保证除数非零且节点结果满足 0 < result < 1。运算符数量在 1 到 3 之间随机选择。
判重使用递归规范键。仅在 +、× 节点中对左右子树键排序,不展平树。因此交换左右操作数得到同一键,而不同结合结构仍可区分,准确覆盖题目给出的特殊规则。
输出表达式时根据优先级及左右位置添加必要括号。批改时使用递归下降解析器,不使用有安全风险的 eval。普通分数和带分数都会转换成 Fraction 后比较,所以约分形式不同仍能正确判定。
三、关键代码说明
关键实现位于:
arithmetic.py的Expression.binary:集中维护减法与除法约束。arithmetic.py的Expression.canonical_key:按交换律判重。arithmetic.py的ExerciseGenerator.generate:使用集合实现近似 O(1) 的重复检测。arithmetic.py的ExpressionParser:处理优先级、括号和四种运算。Myapp.py的main:区分生成模式与批改模式并校验参数。
四、效能分析
测试环境为 Python 3.12,参数为 -r 10,每组运行 5 次取中位数。生成 10、100、1000、10000 道题分别耗时 0.000095、0.000992、0.010110、0.128430 秒,增长趋势接近线性。
题目生成性能
cProfile 对一万题的分析记录了 1,810,928 次调用,总耗时 0.441 秒(分析器本身有额外开销)。累计耗时最高的业务函数是 _expression(0.380 秒),之后是 _number(0.182 秒)。改进点是使用集合保存规范键,避免每生成一道题都线性扫描已有题目;同时在构建节点时直接缓存计算结果,渲染和判重不再重复求值。
性能分析及改进耗时:35 分钟。其中性能数据采集与 cProfile 分析约 15 分钟,判重结构和结果缓存优化约 15 分钟,优化后复测约 5 分钟。
五、测试运行
执行 python -m pytest -q,结果为 18 passed。此外,一万题实测成功生成,随后用程序自身批改生成结果,得到 Correct: 100、Wrong: 0(端到端抽取 100 题回批)。主要用例如下:
| 编号 | 场景 | 预期结果 |
|---|---|---|
| 1 | 整数、真分数、带分数格式化 | 输出规范且可还原 |
| 2 | 1 + 2 × 3 |
结果为 7 |
| 3 | (1 + 2) × 3 |
结果为 9 |
| 4 | 负数减法节点 | 拒绝创建 |
| 5 | 除法结果大于等于 1 | 拒绝创建 |
| 6 | 1+2 与 2+1 判重 |
规范键相同 |
| 7 | 题目给出的三种结合结构 | 前两者重复,第三者不重复 |
| 8 | 随机生成 500 道 | 运算符不超过 3 且无重复 |
| 9 | 递归检查所有中间节点 | 减法非负、除法为真分数 |
| 10 | 答案正确、错误、缺失、非法 | 分类与编号正确 |
| 11 | 生成模式缺少 -r |
显示帮助并非零退出 |
| 12 | 生成 20 道题 | 两个输出文件各 20 行 |
| 13 | 批改文件 | 生成指定格式的 Grade.txt |
| 14 | 只给 -e 未给 -a |
显示参数错误 |
| 15 | -r 1 -n 10 极端范围 |
正常生成且不死循环 |
正确性来自三方面:所有运算使用标准库有理数保证精确;构造阶段递归维护题目约束;生成、文本化、重新解析和批改组成了端到端闭环测试。
六、项目小结与结对感受
本项目完成了题目生成、答案计算、表达式判重和答案批改等需求,并通过一万道题目的性能测试。实现过程中最关键的收获,是认识到这道题并不只是“随机拼接数字和运算符”。减法和除法的限制需要对每一棵子表达式成立;重复题目的判断也不能仅比较最终结果或表达式字符串。因此,我们使用表达式树描述题目,在创建节点时计算并保存精确值,同时使用递归规范键处理加法和乘法的交换律。这样的设计让生成、计算、显示和判重都建立在同一种数据结构之上,减少了不同模块之间规则不一致的问题。
项目做得比较成功的地方有三个。第一,所有计算均使用 Fraction,避免了浮点数误差,普通分数和带分数也可以统一处理。第二,程序没有使用 eval 批改答案,而是实现了递归下降解析器,能够正确处理括号和运算优先级,也降低了读取外部文件时的安全风险。第三,测试不仅检查单个函数,还覆盖了命令行、文件生成、错误参数、一万题生成以及生成后重新批改的完整流程。测试帮助我们发现并修正了 Windows 子进程输出编码问题,也验证了程序在 -r 1 等边界条件下不会陷入死循环。
项目仍有可以改进的地方。当前生成器采用随机生成后判重的方式,当数值范围很小而题目数量很大时,重复概率会上升。虽然程序通过最大尝试次数避免了无限循环,并会提示用户增大范围或减少数量,但以后可以为小范围建立可用表达式枚举或容量估算,更早判断请求是否可完成。此外,当前程序主要通过 Python 脚本和批处理文件运行,后续还可以在发布阶段自动构建独立的可执行文件,并在持续集成环境中自动运行测试。
陈宇轩的结对感受:
这次结对开发让我体会到,先统一需求理解比直接编码更重要。开始时我更关注随机生成过程,而李家乐提醒我重点检查“中间计算不能出现负数”和“除法结果必须为真分数”是对子表达式的约束。我们随后决定把约束集中到表达式节点的创建过程,而不是等整道题生成后再补救,这使代码结构更清楚,也方便了测试。
李家乐在结对过程中的闪光点是对边界条件比较敏感。特别是在交换律判重、括号结构和批改文件漏答等问题上,李家乐能够从题目示例继续推导其他可能出错的情况,并把它们转化为测试用例。给李家乐的建议是,在提出边界问题时可以同时记录预期结果和最小复现示例,这样我们讨论和定位问题会更快。
李家乐的结对感受:
我在这次项目中进一步理解了模块划分和自动化测试的价值。陈宇轩将命令行处理、表达式领域逻辑和文件读写分开,使我在检查某一项功能时不需要同时理解所有代码。性能分析也让我看到,优化之前应该先找到实际耗时较多的函数;使用集合保存规范键后,一万题生成仍能保持较好的速度,比凭感觉进行优化更可靠。
陈宇轩在合作中的闪光点是能够快速把讨论结果落实为清晰的代码结构,并在修改核心逻辑后及时补充回归测试。遇到意见不一致时,陈宇轩会用题目中的具体示例和测试结果验证方案,而不是只凭个人习惯决定。给陈宇轩的建议是,后续可以在编码前把接口和异常处理约定写得更详细,减少实现过程中对参数边界和错误提示的反复调整。
总体来看,这次结对过程形成了“共同分析需求、分工实现、交叉检查、测试验证”的工作方式。两个人关注点不同:一方更重视结构和实现效率,另一方更重视规则细节和异常场景。这种互补减少了遗漏,也让我们认识到,高质量的软件不仅要能运行,还需要可验证、可维护,并能够清楚说明设计选择。
浙公网安备 33010602011771号