1. Github地址及项目成员
-
Sefue:https://github.com/zhengjinhuai/arithmetic-generators
-
我的小伙伴:https://github.com/jezing/arithmetic-generators
-
郑进怀 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),发现了另一个优化空间,就是在随机生成的过程中,就已经可以保证不会生成一样算式(即复合需求,(1 + 2) + 3和3 + (1 + 2)并不会同时产生),于是最后的查重表达式就变成了我们佐证的一个工具!最后我们写了一个测试(测试是否会出现重复的题目),分别测试了1W道题目,和100W道题目的时候产生的重复题目,结果抛出重复的异常为0。
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. 项目总结
本次结对编程收获颇多,"1 + 1 > 2"怕说的就是结对编程了。两个人一起分析题目考虑的情况比单个人的多,因此本次项目我们的算法也经得测试。分工合作,加快开发效率的同时也减少了更多的潜在错误。
本次项目采用的是命令行窗口,因此相对普通,代码方面争取在下次练习中运用更多python的特性和语法糖以提升程序性能。