结对作业
| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS |
|---|---|
| 这个作业要求在哪里 | https://edu.cnblogs.com/campus/gdgy/Class78-Grade2024-CS/homework/15703 |
| 这个作业的目标 | 通过结对合作实现一个自动生成小学四则运算题目的命令行程序,同时练习分工协作、沟通配合 |
一、项目信息
| 队友 | 姓名 | 学号 | github项目地址 |
|---|---|---|---|
| 队友1 | 钟楚楠 | 3224004271 | https://github.com/ChloeZhongN/arithmetic-generator |
| 队友2 | 易诗婷 | 3224004347 | https://github.com/ChloeZhongN/arithmetic-generator |
二、准备
2.1 PSP表格
| PSP2.1 Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|
| Planning(计划) | 10 | 20 |
| · Estimate(估计这个任务需要多少时间) | 10 | 20 |
| Development(开发) | 200 | 265 |
| · Analysis(需求分析,包括学习新技术) | 30 | 35 |
| · Design Spec(生成设计文档) | 20 | 25 |
| · Design Review(设计复审) | 10 | 10 |
| · Coding Standard(代码规范,为目前的开发制定合适的规范) | 10 | 8 |
| · Design(具体设计) | 30 | 35 |
| · Coding(具体编码) | 80 | 100 |
| · Code Review(代码复审) | 10 | 12 |
| · Test(测试:自我测试,修改代码,提交修改) | 30 | 40 |
| Reporting(报告) | 60 | 75 |
| · Test Report(测试报告) | 30 | 40 |
| · Size Measurement(计算工作量) | 10 | 5 |
| · Postmortem & Process Improvement Plan(事后总结,并提出过程改进计划) | 20 | 30 |
| 合计 | 270 | 360 |
2.2 需求分析
本程序的目标是实现一个小学四则运算题目自动生成与批改的命令行工具,支持题目生成、答案生成和答案批改三个核心功能。
- 支持自然数、真分数、带分数
- 支持+、−、×、÷和括号
- 每道题运算符个数不超过3个
- 减法不能产生负数
- 除法结果必须是真分数
- 生成的题目不能重复
- 使用-n指定生成题目数量
- 使用-r指定数值范围,且必须给定
- 题目保存到Exercises.txt,答案保存到Answers.txt
- 支持批改:-e 题目文件,-a 答案文件,统计结果输出到Grade.txt
- 支持一次生成一万道题目
三、代码分析
3.1 效能分析与性能改进
在本阶段项目中,为了验证程序在较大规模数据量下的稳定性,我们对项目核心的“一万道四则运算题目生成”功能进行了效能分析(测试参数:-n 10000 -r 50)。
1. 优化前的性能分析
我们使用 Python 自带的 cProfile 性能分析工具对初版代码进行了运行监测,并通过 pstats 提取了累计耗时最核心的前 20 个函数,得到优化前的性能报告(图一):
图一:初版代码生成 10000 道题目的 cProfile 性能分析结果
-
消耗最大的函数展示:
-
从图一的数据中可以看出,生成 10000 道题目耗时约 0.648 秒。
-
消耗时间最多的核心函数是
generate_problems(累计耗时cumtime = 0.511s),其底层的递归构建子函数try_build和_build也是主要的性能开销点(累计耗时分别为0.448s和0.443s)。 -
此外,频繁调用的随机数生成函数
random.randint占用了相当一部分计算开销。
2. 性能改进思路与实施
针对上述性能瓶颈,我们分析认为:在 Python 中,如果频繁在递归深处通过 random.randint 或 random.choice 调用全局模块的方法,会产生额外的命名空间查找开销。
- 改进方案:我们在
generator.py内部引入了局部变量绑定机制,将高频使用的随机函数在局部进行引用,减少全局查找带来的性能损耗。
3. 优化后的性能对比
经过局部变量优化后,我们再次运行相同的压力测试(-n 10000 -r 50),得到了优化后的性能报告(图二):
图二:代码优化后的 cProfile 性能分析结果
-
对比与结论:
-
对比前后两张图可以看出,优化后的总执行耗时由原来的 0.648 秒轻微下降至 0.644 秒。
-
从程序中消耗时间最长的前三大核心函数来看,它们的累计耗时(cumtime)均实现了不同程度的下降:
generate_problems:累计耗时由 0.511 秒优化至 0.502 秒;try_build:累计耗时由 0.448 秒优化至 0.442 秒;_build:累计耗时由 0.443 秒优化至 0.436 秒。
-
randint相关的单次调用及整体递归构建效率得到了精简,验证了局部缓存策略在应对高频生成场景时的优化效果,确保了程序在满足“一万道题目生成”需求时具备良好的响应速度和稳定性。
3.2 设计实现过程
本项目采用面向对象与模块化设计思想,整体划分为 mathquiz 核心包与根目录入口 main.py。系统由 6 个核心类与若干功能模块组成,职责分明,具体架构如下:
1. 核心类与模块职责划分
-
Rational类(分数与有理数计算层):- 自动约分与规范化:初始化时利用欧几里得算法(
gcd)自动约分,确保分母为正、负号上移。 - 四则与比较运算:重载
+、-、×、÷及各类比较符,支持交叉相乘比较大小。 - 多格式互转:实现
__str__与parse(),支持整数、真分数(如3/5)与带分数(如2’3/8)的输入输出;支持__hash__以便放入集合去重。
- 自动约分与规范化:初始化时利用欧几里得算法(
-
Expr表达式树与节点(语法树层):Expr(抽象基类):定义求值(eval)、优先级(priority)、字符串转换(to_string)与规范化(canonical)四大接口。Num(数字节点):包装Rational对象,作为表达式树的叶子节点。Op(运算符节点):持有左右子树与运算符。其核心亮点在于canonical()方法:对满足交换律的+和×进行左右子树归一化排序,为题目查重提供唯一标识。
-
Parser类(表达式解析层):- 采用递归下降语法解析(Recursive Descent Parsing),通过
_parse_expr、_parse_term、_parse_factor处理运算符优先级与括号嵌套,将题目字符串反序列化为可执行的表达式树。
- 采用递归下降语法解析(Recursive Descent Parsing),通过
-
generator模块(题目生成器):- 防负数与真分数约束:在递归构建二叉树(
_build)时,动态过滤减法(确保被减数 >= 减数)与除法(确保结果为真分数)。 - 高效查重:利用表达式树的
canonical()字符串指纹结合set集合,实现一万道题目的不重复生成。
- 防负数与真分数约束:在递归构建二叉树(
-
grader模块(自动批改器):- 逐行读取题目与答案文件,借助
Parser还原标准答案并与学生答案(Rational.parse)进行对等比较,统计对错题号并输出Grade.txt。
- 逐行读取题目与答案文件,借助
-
cli与main模块(命令行交互层):- 解析
-n、-r、-e、-a等命令行参数,提供帮助提示(-h)与异常捕获。
- 解析
2. 模块调用与依赖关系
各模块之间的核心协作链路如下:
- 程序入口:
main.py-> 调用cli.main()。 - 生成链路:
cli-> 调用generator-> 依赖Rational生成操作数、依赖Num/Op构建表达式树。 - 批改链路:
cli-> 调用grader-> 依赖Parser解析题目、依赖Rational进行精确比对。 - 底层依赖:
Rational为最底层支撑类,所有涉及数值运算与解析的模块均对其高度依赖。
3.核心函数流程图
在程序的所有核心函数中,最关键的是负责递归构建表达式树的 _build 函数。它把控了小学四则运算的各项数学约束(如防负数、真分数限制等),其执行流程如下:
3.3 代码说明
在本项目的实现中,我们采用了模块化的设计思想(划分为表达式树、随机生成器、解析器、分数计算及批改模块)。以下展示项目中两个最关键的核心代码片段及其实现思路:
1. 核心代码一:题目查重机制(基于二叉树规范化)
- 代码片段(来自
expression.py中的Op类的canonical方法):
def canonical(self):
"""
返回表达式的规范形式字符串,用于判重
"""
l = self.left.canonical()
r = self.right.canonical()
# 加法和乘法满足交换律,可以对左右子树进行排序归一化
if self.op in ('+', '×') and l > r:
l, r = r, l
return f"({l}{self.op}{r})"
- 设计思路与注释说明:
- 解决交换律重复问题:题目要求形如
23 + 45 =和45 + 23 =,或者6 × 8 =和8 × 6 =属于重复题目。 - 规范化归一化(Canonical Form):我们为表达式树中的每个节点设计了
canonical()方法。当遇到加法(+)或乘法(×)时,由于它们满足交换律,我们通过比较左右子树的规范化字符串l和r的大小,将较小的字符串排在左边、较大的排在右边。
- 解决交换律重复问题:题目要求形如
- 这样一来,无论随机生成的顺序如何,等价的表达式最终都会被转化为相同的字符串,进而能够直接放入
set()集合中实现准确的查重。
2. 核心代码二:分数类自动约分与带分数格式化
- 代码片段(来自
rational.py中的Rational类构造与输出):
class Rational:
def __init__(self, num, den=1):
if den == 0:
raise ValueError("分母不能为 0")
# 保证分母为正,负号统一上移
if den < 0:
num, den = -num, -den
# 利用辗转相除法求最大公约数,构造时自动约分
g = gcd(abs(num), den)
self.num = num // g
self.den = den // g
def __str__(self):
# 满足题目要求的真分数、带分数、整数输出格式
if self.den == 1:
return str(self.num)
if abs(self.num) < self.den:
return f"{self.num}/{self.den}"
# 转换成带分数格式(如 2’3/8)
k = abs(self.num) // self.den
r = abs(self.num) % self.den
sign = -1 if self.num < 0 else 1
if r == 0:
return str(sign * k)
return f"{sign * k}’{r}/{self.den}"
- 设计思路与注释说明:
- 精准算术与自动约分:为了防止浮点数精度丢失,所有数值(自然数、真分数)均封装为自定义的
Rational(有理数)类。在初始化时,通过math.gcd自动求最大公约数并约分,保证每个分数始终处于最简状态。 - 格式化输出适配:重写了
__str__方法,完美支持题目要求的输出规范:分母为 1 时输出整数,分子小于分母时输出如3/5的真分数,超过时自动转换为如2’3/8的带分数格式。
- 精准算术与自动约分:为了防止浮点数精度丢失,所有数值(自然数、真分数)均封装为自定义的
3.4 测试运行
1. 测试用例 1:基础题目生成(常规参数)
-
测试指令:生成 5 道数值范围在 10 以内的四则运算题目。
python main.py -n 5 -r 10 -
测试预期:程序成功运行,无报错,并在当前目录下生成
Exercises.txt和Answers.txt文件。 -
实际运行结果:
终端提示:成功生成5道题目,已保存到 Exercises.txt和Answers.txt
Exercises.txt输出内容示例:
Answers.txt输出内容示例:
- 正确性说明:程序能够正确处理整数、真分数及带分数的混合运算,生成的表达式格式规范,括号优先级和等号完整,答案计算准确。
2. 测试用例 2:大规模与较宽数值范围生成
-
测试指令:生成 20 道数值范围在 20 以内的四则运算题目。
python main.py -n 20 -r 20
-
测试预期:程序在较大的数值范围(
-r 20)和较多的生成数量(-n 20)下,能够高效完成二叉树递归构建、查重以及文件写入,不发生卡死或重复。 -
实际运行结果:
终端提示:成功生成20道题目,已保存到 Exercises.txt和Answers.txt
Exercises.txt生成了 20 行结构各异、包含多运算符及括号嵌套的合法四则运算题目。
Answers.txt输出了对应的 20 行标准答案(均已自动完成约分与带分数格式化)。
- 正确性说明:验证了程序在高频随机递归和查重校验下的性能与稳定性,确保大批量生成时不会出现重复题目或边界错误。
3. 测试用例 3:极小数值范围边界测试(-r 2)
- 测试指令:生成 5 道数值范围在 2 以内的四则运算题目。
python main.py -n 5 -r 2
-
测试预期:在较小的数值边界(
-r 2,即可选数字包含 0、1)下,程序能够正确收敛并生成合法的边界题目。 -
实际运行结果:
终端提示:成功生成5道题目,已保存到 Exercises.txt和Answers.txt
Exercises.txt生成了 5 行包含 0 和 1 边界值的算术表达式(如1 × 0 - 0 =、(1 + 1) × (0 + 1) =)。
Answers.txt输出了正确的计算答案。
- 正确性说明:验证了程序对边界极值(如 0 和 1)的适应能力以及防崩溃保护机制,证明其在极端输入下依然稳健。
4. 测试用例 4:参数缺失检查测试
- 测试指令:只输入生成数量,不输入范围参数。
python main.py -n 5
-
测试预期:程序检查到缺少
-r参数时,直接给出提示并打印帮助文档,不报错闪退。 -
实际运行结果:
终端显示错误提示:错误:必须指定-r参数.
终端输出了完整的参数使用说明。
- 正确性说明:证明程序能够正确拦截漏传参数的情况,提示信息准确。
5. 测试用例 5:正确答案批改测试
- 测试指令:使用题目文件和对应的标准答案文件进行批改。
python main.py -e Exercises.txt -a Answers.txt
-
测试预期:程序能够读取文件,逐行解析算式并比对答案,正确统计出全对的结果。
-
实际运行结果:
终端提示:批改完成,结果已保存到Grade.txt。
Grade.txt记录内容为:
Correct: 5 (1, 2, 3, 4, 5) 和 Wrong: 0 ()。
- 正确性说明:验证了自动批改模块的文件读取、表达式解析和答案比对功能均能正常工作。
6. 测试用例 6:包含错题的批改测试
- 测试指令:使用包含错误答案的文件进行批改 (修改了测试三 Answer.txt 的第一题和第三题) 。
python main.py -e Exercises.txt -a Answers.txt
-
测试预期:程序能够对比题目和答案,正确识别出错误的题目,并把正确的题号和错误的题号分别写入统计文件。
-
实际运行结果:
终端提示:批改完成,结果已保存到Grade.txt。
Grade.txt记录内容为:
Correct: 3 (2, 4, 5) 和 Wrong: 2 (1, 3)。
- 正确性说明:证明批改模块能够准确核对答案,将错题与对题正确分类并输出。
7. 测试用例 7:文件路径不存在异常测试
- 测试指令:传入一个不存在的题目文件路径进行批改。
python main.py -e not_exist.txt -a Answers.txt
-
测试预期:程序在找不到指定文件时,能够捕获文件操作异常,并输出清晰的错误信息,保证程序平稳退出。
-
实际运行结果:
终端捕获异常并打印提示:批改失败: [Errno 2] No such file or directory: 'not_exist.txt'。
- 正确性说明:验证了批改模块对错误文件路径和 I/O 异常的处理能力,证明程序具备良好的容错性。
8. 测试用例 8:题目与答案行数不一致测试
- 测试指令:使用行数不同的题目文件和答案文件进行批改 (删除了测试6中
Answers.txt的第五题答案) 。
python main.py -e Exercises.txt -a Answers.txt
-
测试预期:程序在检测到题目数量和答案数量不匹配时,能够给出友好的警告提示,不发生崩溃,并对可对齐的部分继续完成批改。
-
实际运行结果:
终端打印警告:警告:题目数 5 与答案数 4 不一致。
批改完成,结果已保存到Grade.txt。
Grade.txt记录内容为:
Correct: 2 (2, 4) 和 Wrong: 3 (1, 3, 5)
- 正确性说明:验证了批改模块对输入文件格式和数量对齐的容错校验机制,证明程序具备处理不完整输入时的鲁棒性。
9. 测试用例 9:批改模式参数缺失测试
- 测试指令:批改模式下只传题目文件参数,故意漏掉答案文件参数。
python main.py -e Exercises.txt
-
测试预期:程序检测到批改模式参数不全时,能够拦截错误,拒绝执行,并打印出错误提示与使用说明。
-
实际运行结果:
终端打印错误提示:错误:批改模式需要同时指定-e和-a.
终端自动输出参数使用说明文档。
- 正确性说明:验证了命令行解析模块对批改模式双参数的联动校验机制,防止用户漏传参数导致程序异常。
10. 测试用例 10:帮助文档查看测试
- 测试指令:请求查看程序的帮助信息。
python main.py -h
-
测试预期:程序能够正确识别
-h参数,输出完整的用法说明、参数释义以及运行示例,不报错。 -
实际运行结果:
终端打印了清晰的帮助文档,包含了-n、-r、-e、-a、-h的功能说明与正确示例。
- 正确性说明:验证了帮助命令的响应机制,确保用户能够随时查阅指令规范。
四、项目小结
1. 成功经验与收获
-
高效的代码评审(
Code Review):通过结对编程,两人共同编写和审查代码,减少了低级语法错误和逻辑漏洞,代码质量得到提升。 -
合理的模块化设计:项目前期明确划分了模块职责(如
Rational分数类、Expr表达式树、Parser解析器、generator生成器及grader批改模块),降低了后期联调的难度。 -
全面的测试用例覆盖:设计并执行了覆盖正常生成、边界条件(如极小数值)及异常处理(如文件不存在、参数缺失、行数不一致)的 10 个测试用例,保证了程序的鲁棒性。
2. 教训与不足
-
前期接口定义不够细致:在项目初期,对部分模块之间的输入输出格式约定不够统一,导致中间联调时出现了一些不必要的返工。
-
数学边界条件考虑滞后:在处理除法结果真分数约束和带分数格式化时,初期对特殊边界的考虑不够全面,后期通过反复调试才完全修复。
3. 经验总结
- 结对项目不仅锻炼了代码实现能力,更重要的是提升了团队沟通效率和规范化开发的意识。明确的分工、及时的代码同步以及全面的测试用例是保障项目顺利完成的核心要素。
4.结对感受
| 队友 | 感想 | 对彼此的闪光点 | 对彼此的建议 |
|---|---|---|---|
| 钟楚楠 | 我们在开发过程中保持了高效的沟通与配合,分工明确且进度同步。面对繁琐的异常处理和边界测试时,能够齐心协力快速排查并解决问题,完成了各项开发与测试任务。 | 写核心代码和底层逻辑比较高效,尤其是表达式树和查重部分,写得很严谨扎实。 | 开始前可以先把接口和数据格式统一说定,这样后面联合调试的时候能省更多时间 |
| 易诗婷 | 这次结对项目整体配合比较顺利。我们按模块分工,一人负责核心实现,一人负责测试和性能,接口约定清楚之后,彼此的工作基本不冲突。 | 测试用例设计得很细致,覆盖了很多边界情况;性能分析做得认真,找出了生成大量题目时的瓶颈 | 可以在开发早期就同步测试思路,这样实现时能提前考虑可测试性 |























浙公网安备 33010602011771号