1. Github地址及项目成员
-
https://github.com/zhengjinhuai/arithmetic-generators(进怀github)
-
https://github.com/jezing/arithmetic-generators(曾霖github)
-
郑进怀 3117004637 ;曾霖 3117004602
2. PSP表格:
PSP2.1 |
Personal Software Process Stages |
预估耗时(分钟) |
实际耗时(分钟) |
Planning |
计划 |
20 |
20 |
· Estimate |
· 估计这个任务需要多少时间 |
20 |
20 |
Development |
开发 |
1560 |
1490 |
· Analysis |
· 需求分析 (包括学习新技术) |
70 |
60 |
· Design Spec |
· 生成设计文档 |
60 |
60 |
· Design Review |
· 设计复审 (和同事审核设计文档) |
80 |
90 |
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
20 |
20 |
· Design |
· 具体设计 |
80 |
90 |
· Coding |
· 具体编码 |
1000 |
905 |
· Code Review |
· 代码复审 |
120 |
120 |
· Test |
· 测试(自我测试,修改代码,提交修改) |
130 |
145 |
Reporting |
报告 |
150 |
150 |
· Test Report |
· 测试报告 |
60 |
70 |
· Size Measurement |
· 计算工作量 |
30 |
30 |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
60 |
50 |
合计 |
|
1730 |
1660 |
3. 效能分析
输入:-r为10,-n为10000(数字及分数分母范围为10以内,题目数目为10000道)
优化前:程序运行总时间为3.002s
分析及优化过程:
-
观察性能分析结果发现程序耗时的地方在于随机生成运算数和计算过程
-
除此之外还发现存在冗余计算,同时fraction分数模块和IO读写所占时间较大。
因此,优化过程主要从以上几个方面着手。
优化后:程序运行总时间为1.727s
图1:优化前性能分析
图2:优化后性能分析
在优化效能的过程中,我们经历了一段很长的讨论(纠结ing),并且在最后的时间内,我们在重新分析需求的时候发现了自己的一个很大的性能优化问题,就是我们在生成的过程中,就已经可以保证不会生成一样(即复合需求)算式,于是最后的查重表达式就变成了我们佐证的一个工具!于是我们又在写了一个测试类(测试是否会出现重复的题目),分别测试了10000道题目,和100W道题目的时候 产生的重复题目,结果如图所示
1.10000道题目 2.100W道题目
在经过试验几遍以及换另一台机器试验几次的多次试验之后,我们还是得到了上述结果,所以我们在此说我们的这种方法(算式二叉树)的方法时最为NB的方法之一,目前为止没有出现BUG!而且效率也挺高的。小学生不用睡觉系列!
4. 设计实现过程
设计思路和小组讨论:
在阅读题目和分析需求之后,我们组将此次的项目分为三个部分,一是生成运算式并查重;二是计算以及保存题目及答案;最后是记录用户输入答案并且评分。
通过argparse模块获用户输入参数-n,-r等参数设置来运算范围、生成题目数目和是否产生负数等。
前期讨论过程中,我们设想使用简单随机填充数字和操作符来生成算术表达式,以及检测答案不一致这种方法来查重,但是效率较低,也不方便后续做扩展。因此,我们又经过了讨论,决定使用二叉树随机生成运算式。在结点Node类里面设置表达式的结构,二叉树Tree类逐层生成含操作符和操作数的运算树(父节点为操作符,叶子节点为操作数,如下图3)。通过赋予运算符优先级op_priority匹配加减乘除的运算过程,并且结合操作符优先级生成对应的中缀表达式。之后通过后缀表达式来生成查重表达式,来检测运算式是否重复(查重思路参考链接:https://www.cnblogs.com/wxrqforever/p/8679118.html)。
图3:表达式二叉树
接着,结合操作符优先级计算结果,通过FileUtils类将结果保存到exercise.txt文档以及answer.txt文档,最后在控制台中进行答题之后获取成绩grade。
类之间的调用以及各方法解释:
类说明:
-
calc_cmd.py:主程序类,调用其他类运行。
-
calc_error.py:异常类,定义生成中以及整个运行过程中产生的一些异常
-
calc_util.py:计算类,用于计算算式,并返回相关的异常,进行评分和收集答案
-
expre_tree.py:定义算式二叉树以及结点类的生成方法以及属性等
-
FileUtils.py:文件类,用于储存题目exercise.txt,answer.txt,grade.txt文件
-
FormatUtils.py:生成表达式类如(后缀表达式以及查重表达式),并且将分数转换成真分数形式
-
unit_text.py:测试用类
5. 代码说明
主类中的主函数方法,通过调用各个类的参数进行提交
1 parser = argparse.ArgumentParser(description="四则运算") 2 parser.add_argument('-n', dest='number', type=int, default=1, help='number of generated questions') 3 parser.add_argument('-r', dest='range', type=int, default=10, help='range of values') 4 parser.add_argument('-e', dest='exercise', type=str, help='formula expression file') 5 parser.add_argument('-a', dest='answer', type=str, help='answer expression file') 6 parser.add_argument('-g', dest='grade', type=str, help='grade file') 7 parser.add_argument('-m', dest='minus', default=False, action='store_true', 8 help='produce formulas with negative numbers') 9 args = parser.parse_args() 10 11 12 if __name__ == '__main__': 13 if args.range is None: 14 print("请输入'-r'参数控制题目中数值(自然数、真分数和真分数分母)的范围") 15 if args.exercise is None: 16 args.exercise = os.path.join(os.getcwd(), 'Exercises.txt') 17 if args.answer is None: 18 args.answer = os.path.join(os.getcwd(), 'Answer.txt') 19 if args.grade is None: 20 args.grade = os.path.join(os.getcwd(), 'Grade.txt') 21 print("欢迎进入答题模式......(输入'exit'可退出程序)") 22 t = Tree() 23 u_answer = list() # 用户答案 24 formula, s_answer = t.generate_formula(args.range, args.number, args.minus) # 随机生成表达式 25 FileUtils.write_file(formula, s_answer, args.exercise, args.answer) # 保存题目文件 26 for i in range(args.number): 27 print(formula[i], end='') 28 answer = input() # 获取用户输入的答案 29 if answer == 'exit': 30 print('退出程序成功!') 31 sys.exit() 32 u_answer.append(answer) 33 correct, wrong = CalculatorUtils.grading(u_answer, s_answer) # 统计答题结果 34 print("答题结果:") 35 print(correct) 36 print(wrong) 37 FileUtils.write_grade_file(args.grade, correct, wrong) # 保存答题结果
自定义异常类,包括不为负数、分母不超过某个值,以及运算重复的异常类对象,方便以后做扩展
class NegativeError(Exception): """自定义表达式不为负数的异常类""" def __init__(self): super(NegativeError, self).__init__() # 初始化父类 def __str__(self): return class DifficultError(Exception): """自定义分母不能超过某个值的异常类""" def __init__(self): super(DifficultError, self).__init__() # 初始化父类 def __str__(self): return class DuplicateError(Exception): """自定义异常类""" def __init__(self): super(DuplicateError, self).__init__() # 初始化父类 def __str__(self): return
随机生成四则运算表达式
def generate_formula(self, num_range, number, negative): """随机生成式子""" num = 0 degree = random.randrange(3, 4) # 随机设置操作数的个数 while num < number: empty_node = [self.root] for _ in range(degree): '''生成操作符号节点''' node = random.choice(empty_node) empty_node.remove(node) node.operator = random.choices(self.op_list, cum_weights=self.op_weight)[0] # node.operator = random.choices(self.op_list)[0] node.type = 2 # 每生成一个操作符号节点,生成两个空节点 node.left = Node() node.right = Node() empty_node.append(node.left) empty_node.append(node.right) for node in empty_node: '''将所有空结点变为数字结点''' node.type = 1 # 设置真分数的比重 1为整数 0为分数 num_type = random.choices(self.type_list, self.num_weight)[0] if num_type == 1: # 生成一个整数 node.number = random.randint(1, num_range) else: # 生成一个真分数 node.number = Fraction(random.randint(1, num_range), random.randint(1, num_range)) try: # self.root.show_node() # 获取生成的二叉树结构 self.root.get_answer(negative) # 计算答案 if self.root.number.denominator > 99: # 分母超过99抛出异常 raise DifficultError() self.pre_formula = self.root.get_formula() # 获取前缀表达式 self.post_formula = FormatUtils.get_result_formula(self.pre_formula) # 获取后缀表达式 self.check_formula = FormatUtils.get_check_formula(self.post_formula) # 获取查重表达式 # 进行查重 if not Tree.duplicate_check(self.check_formula, self.result_formula): # 返回false 则表明没有重复 self.result_formula.append(self.check_formula) else: raise DuplicateError output = FormatUtils.standard_output(self.pre_formula) # 格式化前缀表达式 if isinstance(self.root.number, Fraction): answer = FormatUtils.standard_format(self.root.number) # 格式化答案 else: answer = self.root.number # print(output, answer) self.formula.append(output) self.answer.append(answer) except ZeroDivisionError: # print("除数为零,删除该式子") continue except NegativeError: # print("出现负数,删除该式子") continue except DifficultError: # print("题目较难,删除该式子") continue except DuplicateError: # print("题目重复,删除该式子") continue else: num += 1 return self.formula, self.answer
计算类,可以进行操作数的运算,即生成式子之后的运算结果保存以及抛出相对应的一个异常对象
还包括了返回一个后缀表达式,和最后的评分格式。
1 class CalculatorUtils: 2 3 @staticmethod 4 def eval_formula(operator, a, b, negative=False): 5 """计算简单的加减乘除, 同时抛出不符合题目要求的异常""" 6 answer = 0 7 if operator == "+": 8 answer = a + b 9 elif operator == "-": 10 if a < b and negative is False: 11 raise NegativeError() # 抛出结果为负数的异常对象 12 else: 13 answer = a - b 14 elif operator == "*": 15 answer = a * b 16 elif operator == "/": 17 if b > 99: 18 raise DifficultError() # 抛出题目较难的异常对象(分母大于100) 19 else: 20 answer = a / b 21 # 如果答案为浮点数,则转换为分数形式 22 if isinstance(answer, float): 23 answer = Fraction(a) / Fraction(b) 24 return answer 25 26 @staticmethod 27 def get_answer(formula_list, negative): 28 """计算后缀表达式的结果""" 29 num_list = list() 30 for i in range(len(formula_list)): 31 if isinstance(formula_list[i], int) or isinstance(formula_list[i], Fraction): 32 num_list.append(formula_list[i]) 33 else: 34 b = num_list.pop() 35 a = num_list.pop() 36 res = CalculatorUtils.eval_formula(formula_list[i], a, b, negative) 37 num_list.append(res) 38 return num_list.pop() 39 40 @staticmethod 41 def grading(user_ans, ans_list): 42 """评分,同时返回要求的评分输出格式""" 43 correct = list() 44 wrong = list() 45 length = len(user_ans) 46 for i, u, ans in zip(range(1, length + 1), user_ans, ans_list): 47 if u == ans: 48 correct.append(i) 49 else: 50 wrong.append(i) 51 return correct, wrong
6. 测试运行
分别测试生成前缀表达式:
测试生成后缀表达式:
测试生成查重表达式:
测试生成运算式
测试答题情况及保存文件
7. 项目总结
在经过了整个项目的洗礼之后,作为项目实现的其中一员,整个项目下来学习到了新的知识,新的语言结构以及数据结构,大佬带着小弟闯荡四方,我出谋划策(瞎说了很多想法),贡献了策略,代码量大部分都是进怀写的,我负责的是一些检测和修改,整个项目下来,我发现了很多知识点上的漏洞,但是也学习到了很多东西,比如说进怀使用的这种二叉树的结构,使我对二叉树有了新的理解,以及对于中缀表达式和后缀表达式的理解也更加深刻。无他,就想猛的夸赞一下队友,太给力了。我们队的这次项目,选择的算法简直无敌,太给劲了。本次项目的缺点也许就是没有一个比较完整的GUI界面,使用的是命令行窗口,但是麻雀虽小,算法和数据结构都不小。但是前期开发经验的不足也是我们的代码冗余度更高,特别是我的原因对需求的分析有一些失误,但是后面知道了自己的错误并且及时修改了意见,希望下次能更好的完成任务,并且能多写写代码。感谢队友!