1 作业摘要与结对伙伴介绍
1.1 作业摘要
这个作业属于哪个课程 | 所属课程链接 |
---|---|
这个作业要求在哪里 | 作业要求链接 |
这个作业的目标 | <熟悉软件工程流程,把握PSP流程框架,精进测试代码和性能改进的能力> |
1.2 结对伙伴信息
姓名 | 学号 |
---|---|
彭学智 | 3121004878 |
许铭益 | 3121004883 |
2 PSP表格时间估计
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 45 |
Estimate | 估计这个任务需要多少时间 | 20 | 35 |
Development | 开发 | 60 | 85 |
Analysis | 需求分析 (包括学习新技术) | 120 | 180 |
Design Spec | 生成设计文档 | 20 | 15 |
Design Review | 设计复审 | 30 | 45 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 20 | 15 |
Design | 具体设计 | 120 | 150 |
Coding | 具体编码 | 120 | 180 |
Code Review | 代码复审 | 30 | 25 |
Test | 测试(自我测试,修改代码,提交修改) | 120 | 150 |
Reporting | 报告 | 90 | 125 |
Test Report | 测试报告 | 30 | 30 |
Size Measurement | 计算工作量 | 20 | 30 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 20 | 40 |
合计 | 820 | 1120 |
3 效能分析
3.1 生成四则运算题目功能的性能分析
使用line-profile函数对生成四则运算题目功能部分的代码进行了性能分析,综合上面四张分析结果所示,耗费时间从长到短依次是,从库调用函数、对命令行参数进行设置和解释、一万道题目生成的过程、题目与答案的写入。
外部包的调用无法优化,故在此处无法提高性能。
我们在设计代码中,对于命令行参数增加了“required = Ture”,来对未在命令行输入的指令进行检测和报错,而在参数解释过程中进行的参数检验和错误处理,会增加解释的时间开销,因此在此处我们删除了“required = Ture”,取而代之的是在主函数生成功能中,增加了if-else的嵌套判断来对于命令行参数不正确输入格式进行检验,采取了空间来换取时间的方法,来提高性能。
在主函数的生成过程中,我们在设计代码中采用了递归方式,来生成式子,此处可能改进成二叉树对代码的性能提升更有帮助。
对于写入文本的部分,我们从两个方面提高了读写性能。
第一部分,我们增加了一小部分生成函数的时间消耗,从而优化了写入文本的速度。原先在具体设计的过程中,还仍未考虑要生成一万道题目的现实情景,因此在生成四则运算题目的功能时候采用的是每次生成就写入文本的方式。但是随着每次生成题目越多,生成数的范围越大,我们在对算法进行性能测试时,发现它的消耗时间占比不断提高。在此处提升性能方面,我们改进了生成四则运算题目和写入文件的交互的部分,在生成四则运算题目中增加了数组用来存储每次生成的题目和计算出来的答案,将要写入文本中的数据累积到缓冲区中,在按照命令行所需要求,生成完题目和答案的时候,再调用生成题目和答案的数组统一写入到txt文件中。这样的做法虽然提高了生成四则运算题目函数的耗时,但是大大减少文件打开和关闭的次数,极大程度减少了写入文本的时间。
第二部分,我们采用上下文管理器:使用 with 语句和上下文管理器来确保文件在退出时被正确关闭,以避免资源泄漏和提高性能。
3.2 判定答案对错功能的性能分析
使用line-profile函数对判断答案对错功能部分的代码进行了性能分析,综合上面四张分析结果所示,耗费时间从长到短依次是,从库调用函数、对命令行参数进行设置和解释、对错结果序号的输出、题目与答案的写入。
其中除了结果的输出,其他三个与3.1的性能分析与改进方法一致,此处不过多赘述。
而对错结果序号的输出,由于本身生成题目多少的数量多少而影响,无法提高print的输出性能,故此处无法改进。
3.3 消耗最大的函数
代码如下:
# 1、生成算术表达式
def generate_expression(min_num, max_num, max_depth):
if max_depth == 0: # 递归深度为零,直接返回一个随机数。
return str(random.randint(min_num, max_num))
global ture
# 随机选择一个运算符
operator = random.choices(operators, weights=operator_weights)[0]
# 对于加、减、乘、除运算,递归生成两个子表达式并组合它们
left_expr = generate_expression(min_num, max_num, max_depth - 1)
right_expr = generate_expression(min_num, max_num, max_depth - 1)
if ture == 1: # 上一层递归已经出现了除零报错的时候直接返回
return
if operator == '/': # 检查本层递归是否出现除零报错
try:
x = f'({left_expr} {operator} {right_expr})'
eval(x)
except ZeroDivisionError:
# print("出错,重新生成题目。")
ture = 1
return
if operator == '-': # 当被减数小于减数时,交换两个数字的值,保持被减数大于减数
if eval(left_expr) < eval(right_expr):
translation = left_expr
left_expr = right_expr
right_expr = translation
# 检查左表达式是否包含括号,如果是则不执行 int() 转换
if not re.search(r'\(.*\)', left_expr):
left_expr = int(left_expr)
return f'({left_expr} {operator} {right_expr})'
4 设计实现过程
4.1 函数介绍
本次的结对项目总共包含了6个函数,其中1个是主函数,1个是生成四则运算题目的函数,其余的4个是检查答案的函数。
4.1.1 导入库和定义全局变量:
导入了必要的库,包括argparse用于命令行参数解析,random用于生成随机数,Fraction用于处理分数,re用于正则表达式处理。定义了全局变量ture,用于标记是否出现除零错误。
4.1.2定义四则运算符号和运算符号的权重:
定义了四则运算的符号(+、-、*、/)和它们的权重。
4.1.3 函数generate_expression
生成四则运算表达式的函数,支持指定最小值、最大值和最大递归深度。递归地生成表达式,并处理除零错误。
4.1.3 函数read_problems
从题目文件中读取题目数据的函数。
4.1.4 函数read_answers
对比答案是否正确的函数,将正确和错误的题目索引分别返回。
4.1.5 函数write_grade
将结果写入Grade.txt文件的函数。
4.1.6 函数process_answer
处理答案字符串,将整数、分数和带分数部分统一格式化为分数形式。
4.1.7 主程序:
使用argparse解析命令行参数,根据参数的不同执行不同的功能。如果指定了-n和-r参数,则生成题目和答案,并保存到文件。如果指定了-e和-a参数,则读取题目和答案文件,核对答案,并将结果写入Grade.txt文件。
4.2 关键函数流程图
函数generate_expression的流程图如下:
函数process_answer的流程图如下:
5 代码说明
5.1 函数generate_expression的说明
这个函数递归地生成随机的四则运算表达式,控制递归深度,处理除零错误,确保表达式的合法性,然后返回生成的表达式字符串。该函数的代码设计的步骤如下所示:
5.1.1 参数
min_num:生成随机数的最小值。
max_num:生成随机数的最大值。
max_depth:递归生成表达式的最大深度。
ture:用于标记是否出现除零错误。
5.1.2 递归终止条件
如果max_depth等于0,表示递归深度已经达到指定的最大深度,此时直接返回一个随机整数字符串,作为表达式的叶子节点。
5.1.3 随机选择运算符
使用random.choices函数从预定义的运算符列表operators中随机选择一个运算符,权重由operator_weights定义。
5.1.4 递归生成子表达式
使用递归方式生成两个子表达式,分别作为运算符的左操作数和右操作数。每个子表达式的深度减1,以控制递归深度。
5.1.5 处理除零错误:
如果当前运算符是除法(/),则在生成子表达式后,检查本层递归是否会出现除零错误。如果出现除零错误,将全局变量ture(实际应该是true或True)设置为1,表示出现错误,并直接返回。
5.1.6 处理减法:
如果当前运算符是减法(-),则在生成子表达式后,检查被减数是否小于减数,如果是,交换两个数字的值,以确保被减数大于减数。
5.1.7 检查是否需要转换为整数
检查左表达式是否包含括号,如果不包含,则将其转换为整数。这可能是为了确保结果是整数,而不是分数。
5.1.8 返回表达式:
最终将生成的左表达式、运算符和右表达式组合成一个完整的表达式,并返回。
5.2 函数process_answer的说明
process_answer函数会根据答案字符串的不同格式,分别处理带分数、分数和整数部分,最终将它们格式化为分数形式,并返回。这样可以统一答案的表示形式,方便后续比较和处理。
5.2.1遍历答案的各个部分并识别其类型
这部分代码首先检查答案字符串中包含的不同部分类型。根据不同部分的特征,识别整数、分数和带分数部分。
5.2.2处理带分数部分
如果答案字符串中包含单引号',表示答案是带分数形式。通过分割答案字符串,分离整数部分和分数部分。将整数部分和分数部分分别解析为整数,并计算出分数的分子。最后,将分数形式返回,格式为分子/分母。
5.2.3处理分数部分
如果答案字符串中包含/,表示答案是分数形式。直接将答案字符串返回,不进行额外的处理。
5.2.4处理整数部分
如果答案字符串不包含单引号和/,表示答案是整数形式。直接将答案字符串解析为整数,并将整数形式返回。
6 测试运行
6.1 代码检测
经过Code Quality Analysis并消除警告
如上图所示,均无问题
6.2 异常处理说明
6.2.1 命令行参数required
前提背景:在命令行参数说明的部分,对每个要输入到命令行参数字符进行了输入判读,即“required = Ture”的语句检测。如果在命令行输入不正确时,便会出现以下报错。
为了对此处的异常报错进行处理,同时对不同功能格式的错误输入,给出相应的正确输入,把“required = Ture”的语句检测去掉,并使用if-else嵌套语句来实现检测。
此时面对同样的错误输入,可以给出相应的正确格式的提醒。
6.2.2 输入格式错误
1、未输入命令行参数引起的系统报错
如上图所示:只需按照系统给出的提示输入正确的格式即可。
2、输入了命令行参数,但未输入完整引起的程序报错
如上图所示:仅仅输入-n和生产题目数量多少N,而未输入-r和取值范围大小R的值,被程序检测出报错,因此print正确的生成四则运算功能的格式。
3、修改报错结果
如上图所示:避免了上述的错误,按照提示输入正确的格式,便能正确执行代码。
6.3 10个用例测试程序
6.3.1 测试1——生成四则运算功能的测试
测试范围:生成题目数量n为20,生成数范围大小为0~20。
测试结果如下图所示:
生成题目和答案在上图的右侧,分别保存在Exercises.txt和Answers.txt。从图中Exercises.txt文件可得,生成的题目为20道、生成的范围在0~20之间、生成的每一个减号表达式都大于0,没有负数的出现、每道题目的运算符最多有3三个、且生成的题目没有重复。综上满足需求1、2、3、5、6。继续结合图中的Answers.txt文件可得,左侧生成的题目均与右侧计算的结果一一对应;观察题目4、6、8、11、15、20,它们均包含除法的式子且计算结果为分数,它们的输出结果都为真分数或带分数综上满足需求4、7。
6.3.2 测试2——判定答案中的对错并进行数量统计功能的测试
测试文本:测试1中生成的四则运算题目与答案
测试结果如下图所示:
根据输出结果和保存的Grade.txt文本可得,成功实现了需求9。
6.3.3 测试3——判定答案中的对错并进行数量统计功能的测试
测试文本:测试1中生成的四则运算题目文本与改错第20题的答案文本
测试结果如下图所示:
将Answers.txt文本里面第20个答案改错为0,可以看出该功能检测出来了,并在Wrong里面记录了下了序号为20,符合我们所更改的部分,进一步验证了实现了需求9。
6.3.4 测试4——生成四则运算功能的测试
测试范围:生成题目数量n为10000,生成数范围大小为0~40。
测试结果如下图所示:
大幅度提高了生成四则运算题目的数量,从上图的结果显示测试结果没有问题,测试结果的范围均符合输入要求的范围,测试功能均满足需求,实现了需求8。
6.3.5 测试5——判定答案中的对错并进行数量统计功能的测试
测试文本:测试4中生成的四则运算题目与答案
测试结果如下图所示:
根据输出结果和保存的Grade.txt文本可得,且10000道题目都正确并输出在正确题目的序号中,说明能实现读取简单的符号和排除换行、空格缩进等干扰,进一步验证了实现了需求9。
6.3.6 测试6——生成四则运算功能的测试
测试范围:生成题目数量n为15,生成数范围大小为0~15。
测试结果如下图所示:
如上图所示,测试结果均满足所需要求。
6.3.7 测试7——判定答案中的对错并进行数量统计功能的测试
测试文本:测试6中生成的四则运算题目与更改多道题目的答案
测试结果如下图所示:
将7道题目正确答案更改了错误答案,再又输出结果和保存的Grade.txt文本可得,程序能成功地检测出错误的答案,并输出倒错误答案的组中,进一步验证了实现了需求9。
6.3.8 测试8——生成四则运算功能的测试
测试范围:生成题目数量n为10,生成数为0。
测试结果如下图所示:
如上图所示,通过测试8来完全检验需求6——题目不能重复的需求,每个题目的计算符号不同,实现了不重复的需求,测试结果均满足需求6。
6.3.9 测试9——生成四则运算功能的测试
测试范围:生成题目数量n为10,生成数为10。
测试结果如下图所示:
从上图的结果显示测试结果没有问题,测试结果的范围均符合输入的范围,测试功能均满足各项需求。
6.3.10 测试10——生成四则运算功能的测试
测试范围:生成题目数量n为10,生成数为10。
测试结果如下图所示:
从上图的结果显示测试结果没有问题,测试结果的范围均符合输入的范围,测试功能均满足各项需求。
7 项目小结
7.1 项目总结
项目的成功之处:
我们满足了项目中的各项需求,并采用了10个测试用例来检验不同需求在正常情况和极端情况下是否仍然可以实现它的功能,测试结果向好,说明我们编写的程序是没有问题的。同时我们还掌握了命令行输入的方式,通过在命令行输入不同的设定参数,以此来实现不同的功能。
项目的不足之处:
项目的可扩展性不足。项目目前只支持生成四则运算题目,而且由于采用递归调用的方式,每次只能生成有3个运算符的式子,在项目的开发和代码的设计之初,选择了递归调用的方式,而没有选择可能更加灵活的二叉树方式去实现,如果在此方向改进的话,可以进一步扩展为支持其他类型的题目生成,以增加其实用性。
总的来说,这个项目的基本功能已经实现,但在功能实现的算法选择上仍有一些改进的空间,代码的健壮性有待提高,通过不断优化和完善,可以提高项目的质量和实用性。
7.2 结对感受
彭学智:
从感受上来说两个人进行结对编程,会给人一种更从容的心态。两个人一起讨论问题,会比一个人的想法往往更加全面一些。由于两个人的思路有的时候会有些碰撞或者说不同,所以比较建议一个人来完成一个小的功能部分,然后由另一个人来测试这个部分。
许铭益:
两个人合作完成一个项目,不像之前一样都是单打独斗,给我带来了很多奇妙的感受。在两人思想上不断地碰撞中,我们就不同需求的不同实现方法产生了分歧,但是我们也求同存异,在以解决问题为前提下,去确定更加合适的方法,不断地讨论中,彼此迸发出的新奇的灵感也在一步步推动着项目的完成。在面对有比较大的框架问题中,我们会合作通过讨论来解决问题,在主题的框架搭建好了后,我们又分工去解决各自该实现的需求。整体的合作过程是十分顺利的。