https://github.com/wapleeeeee/Arithmetic-operationhtml
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 8 |
· Estimate | · 估计这个任务须要多少时间 | 10 | 8 |
Development | 开发 | 655 | 785 |
· Analysis | · 需求分析 (包括学习新技术) | 30 | 35 |
· Design Spec | · 生成设计文档 | 30 | 40 |
· Design Review | · 设计复审 (和同事审核设计文档) | 10 | 15 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 5 | 5 |
· Design | · 具体设计 | 40 | 60 |
· Coding | · 具体编码 | 5h*60 | 7h*60 |
· Code Review | · 代码复审 | 1h*60 | 1.5h*60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 3h*60 | 2h*60 |
Reporting | 报告 | 290 | 330 |
· Test Report | · 测试报告+博客 | 4h*60 | 4.5h*60 |
· Size Measurement | · 计算工做量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 过后总结, 并提出过程改进计划 | 40 | 50 |
合计 | 955 | 1123 |
这个题目最开始是在课堂上何老师提出引发你们的思考,一开始我并无意识到这个题目的复杂性。这个题目能够被划分为如下三个问题:python
expression: (1+2+5)-(3*4)=
求通常四则算数表达式的结果通常采用转化为逆波兰表达式。
该种方法通常思路为:git
判断用户输入状况只需接受用户输入比较统计得分便可。因为要求采用命令行界面完成,该部分主要须要控制及美化命令行界面。github
因为四则运算的规则繁杂,随机生成的算式须要判断各类状况的产生,也就无形之中给测试部分增添了很大压力。看过了《构建之法》第二章的全部测试部分以后,利用其中单元测试的部分对项目进行了统一的测试。详细状况见后文测试部分。算法
主程序main()用于处理命令行输入输出,创建了一个类Equation用于保存每个表达式的属性。如下为类中成员变量及成员方法具体介绍。express
变量名 | 类型 | 功能 |
---|---|---|
equ | string | 由随机产生运算符的函数保存生成的表达式。 |
priority | dict | 存放运算符优先级的字典,用于比较优先级大小。 |
answer | Fraction | 保存最终获得的表达式结果 |
op | list | 运算符库 |
函数名 | 输入 | 输出 | 依赖函数 | 功能 |
---|---|---|---|---|
getEquation | void | string finalstring:表达式函数 | insertBracket | 生成随机表达式,须要调用随机添加括号函数 |
insertBracket | string equ:表达式 | string tmplist:表达式函数 | void | 在原表达式基础上随机添加括号 |
getAnswer | string equ:表达式 | Fraction answer:计算结果 | change_list calculate | 根据表达式计算结果 |
change_list | list 中缀表达式 | list 后缀表达式 | void | 将中缀表达式转化为后缀表达式 |
calculate | list 后缀表达式 | Fraction answer:计算结果 | void | 根据后缀表达式计算结果 |
核心函数为getEquation(生成随机表达式)和getAnswer(计算结果)app
#生成随机等式 def getEquation(self): number = random.randint(2,9) tmpstring = "" tmpop = '' tmpint = 0 for i in range(number): if tmpop == '/': #分数状况 tmpint = random.randint(tmpint+1,9) tmpop = random.choice(self.op[:-1]) elif tmpop == '÷': #除号状况 tmpint = random.randint(1,8) tmpop = random.choice(self.op) else: tmpint = random.randint(0,8) tmpop = random.choice(self.op) #添加到算式中 tmpstring += str(tmpint) tmpstring += tmpop tmpstring = list(tmpstring) #修改最后一个符号为= tmpstring[-1] = '=' tmpstring = ''.join(tmpstring) #加括号 finalstring = self.insertBracket(tmpstring,number) return finalstring
#求算式答案 def getAnswer(self,exp): #将带有分号的表达式化成带分数的list equlist = [] i = 0 while(i < len(exp)-1): if exp[i+1] != '/': equlist.append(exp[i]) i += 1 else: equlist.append(Fraction(int(exp[i]),int(exp[i+2]))) i += 3 #将中缀表达式转化为后缀 new_equlist = self.change_list(equlist) #计算后缀表达式的结果 return(self.calculate(new_equlist))
运行结果以下所示:
dom
《构建之法》第二章中详细说起了好的单元测试的标准。ide
这些思想再加上该项目中表达式内容和形式的变幻无穷,构建出一套合适的测试体系成为了这个项目中不可或缺的重要部分。所以我对表达式类中每个函数详细地构造出一套测试方法。函数
函数名 | 输入 | 输出 | 测试方法 | 备注 |
---|---|---|---|---|
getEquation | void | string finalstring:表达式函数 | 使用python自带eval函数检验式子合法性 | 因为该函数包含insertBracket而且输出相同,只须要测试该函数便可覆盖。 |
getAnswer | string equ:表达式 | Fraction answer:计算结果 | 给出不一样状况的表达式,测试输入输出结果是否相同。 | |
change_list | list 中缀表达式 | list 后缀表达式 | 给出特定中缀表达式,测试可否转化成所期待的后缀表达式。 | 包含于getAnswer,但须要单独测试。 |
calculate | list 后缀表达式 | Fraction answer:计算结果 | 输入指定后缀表达式,匹配结果是否相同。 | 包含于getAnswer,但须要单独测试。 |
测试代码以下:
#对getEquation函数测试 def test_getEquation(self): #随机1000000次 for i in range(1000000): tmpString = self.equation.getEquation()[:-1] #保存生成的算式 tmpString.replace('÷','/') #将没法识别的除号替换 self.assertEqual(type(tmpString),(str or int))
测试了100W次随机生成的字符串,用时30.418s。
getAnswer须要输入一个肯定的表达式,输出表达式的结果,选取了十组测试用例以下(Fraction(a,b)表示a/b):
输入表达式 | 期待返回值 |
---|---|
"(1+2)*3=" | 9 |
"(6+4/5)÷3=" | Fraction(34,15) |
"(8/9-7-2+3)+3*4-2/9=" | Fraction(20,3) |
"3*7-3-6+3=" | 15 |
"5+5/9+6-5÷(6/8+3/6)=" | Fraction(68,9) |
"1-6=" | -5 |
"3÷1+8÷5*4*5-2/9*2=" | Fraction(311,9) |
"(5+(6-3)*3/5)÷7=" | Fraction(34,35) |
"(1+2)*(3*(4+5))=" | 81 |
"4+6*0=" | 4 |
单独运行测试获得结果以下:
change_list函数须要输入一个中缀表达式列表,返回一个后缀表达式列表,同时将列表中字符串类型的数字转化为可运算的整型。
输入中缀列表 | 期待返回列表 |
---|---|
["1", "+", Fraction(2,3), "÷", "3"] | [1, Fraction(2,3), 3, "÷", "+"] |
['4', '*', '0', '*', '5', '÷', '7', '-', '0', '÷', '3'] | [4, 0, '*', 5, '*', 7, '÷', 0, 3, '÷', '-'] |
['2', '-', '6', '+', '4', '+', Fraction(1, 4), '÷', '5', '-', '5'] | [2, 6, '-', 4, '+', Fraction(1, 4), 5, '÷', '+', 5, '-'] |
['6', '*', '7'] | [6, 7, '*'] |
['7', '*', '(', '0', '÷', '4', '-', '5', ')'] | [7, 0, 4, '÷', 5, '-', '*'] |
['0', '*', '7', '+', '(', '8', '+', '7', '*', '6', ')', '÷', '4', '*', '4', '-', '7', '-', '4'] | [0, 7, '*', 8, 7, 6, '*', '+', 4, '÷', 4, '*', '+', 7, '-', 4, '-'] |
['0', '+', '1', '*', '8', '÷', '8', '*', '7'] | [0, 1, 8, '*', 8, '÷', 7, '*', '+'] |
[Fraction(3, 7), '÷', '3', '÷', Fraction(2, 3), '÷', '3'] | [Fraction(3, 7), 3, '÷', Fraction(2, 3), '÷', 3, '÷'] |
[Fraction(6, 7), '+', '0', '*', '(', '6', '-', '(', '3', '-', Fraction(5, 8), ')', ')'] | [Fraction(6, 7), 0, 6, 3, Fraction(5, 8), '-', '-', '*', '+'] |
['(', '4', '+', '8', '-', '4', '-', '3', '+', '2', '*', '0', ')', '÷', '4'] | [4, 8, '+', 4, '-', 3, '-', 2, 0, '*', '+', 4, '÷'] |
运行结果以下:
calculate函数是输入一个正确的后缀表达式,根据这个输入获得肯定结果的方法,因为该方法的分支并很少,因此只选取五组测试用例。
输入后缀列表 | 期待返回值 |
---|---|
[6, Fraction(2, 5), '÷', 2, 3, '÷', '-', 1, 4, '*', '-'] | Fraction(31,3) |
[0, 3, 7, '÷', 6, '÷', 0, '-', '÷', 7, '÷'] | 0 |
[Fraction(8, 9), Fraction(2, 3), '+', 5, 5, '÷', 5, 1, '-', '÷', 7, '*', '+'] | Fraction(119,36) |
[4, 7, 1, '+', Fraction(8, 9), '+', '*', 5, 6, '÷', '-'] | Fraction(625,18) |
[2, 1, '÷', 8, '÷', 8, '*', Fraction(1, 2), '÷', Fraction(5, 8), '÷'] | Fraction(32,5) |
单元测试运行效果以下:
将以上五个单元测试叠在一块儿测试,而且增长了错误状况的判断,让方法鲁棒性更强。测试结果以下:
首先考虑到用户输入会影响效能分析中的时间因素,去掉了主函数中接受用户输入并比较的部分,直接改为由代码随机生成算式而后计算结果。
python中的效能分析工具这是一个很是好用的python性能分析工具的介绍,包含了line_profiler(时间分析)以及memory_profiler(内存分析)等python包。
首先测试10W条
统计出五个函数的运行时间及细节以下:
10W次运行时间15.3507s(33.73%)
10W次运行时间3.30584s(9.26%)
10W次运行时间20.3809s(25.7%)(函数内)
10W次运行时间4.4608s(12.68%)
10W次运行时间6.73093s(18.85%)
共计耗时35.7136s。
分析perhit参数(平均每次调用产生的时间)能够很快速地找到哪些语句占用了程序的大多数时间。
抽取了其中部分perhit较高的参数以下表:
出现函数 | 代码句 | Per Hit |
---|---|---|
getEquation | number = random.randint(2,9) | 8.6 |
getEquation | tmpint = random.randint(tmpint+1,9) | 7.0 |
insertBracket | bracketNum = random.randint(0,1) | 7.7 |
insertBracket | right = random.randint(left+1,length-1) | 6.6 |
getAnswer | equlist.append(Fraction(int(exp[i]),int(exp[i+2]))) | 11.5 |
calculate | tmpStack.append(self.plus(number_x,number_y)) | 15.5 |
calculate | tmpStack.append(self.minus(number_x,number_y)) | 15.5 |
calculate | tmpStack.append(self.multiply(number_x,number_y)) | 11.2 |
calculate | tmpStack.append(self.divide(number_x,number_y)) | 21.0 |
分析上表能够看出,效率不高的几条语句大体能够分为如下三类:
import random -> from random import randint
tmpStack.append(self.plus(number_x,number_y)) -> tmpStack.append(number_x + number_y) tmpStack.append(self.plus(number_x,number_y)) -> tmpStack.append(number_x - number_y) tmpStack.append(self.plus(number_x,number_y)) -> tmpStack.append(number_x * number_y) tmpStack.append(self.plus(number_x,number_y)) -> tmpStack.append(Fraction(number_x,number_y))
作了以上工做后从新跑了10W次后。
运行时间为15.0372s+19.3621s=34.3993s
仅仅只快了1s不到。。
回头看看randint优化的百分比并很少,不考虑在这个上面作文章了。
再看刚刚修改的函数调用部分,虽然提升了很多,但跟其余语句比起来依然惨淡很多:
进一步分析发现每一句话都调用了tmpStack.append(x),因而考虑改变结构:
if tmpValue == "+": tmp = number_x+number_y elif tmpValue == "-": tmp = number_x-number_y elif tmpValue == "*": tmp = number_x*number_y else: tmp = Fraction(number_x,number_y) tmpStack.append(tmp)
再次运行10W次,此次缩短到了13.5085s+17.9572s=31.4657s
运行效率增加了13.50%,虽然跟范文数独博客中的1000%小哥比起来相差甚远,不过考虑到四则运算须要考虑的状况之多以及功能的复杂性,在时间上没有更大的优化的空间了。
本次项目虽然核心算法要求并不难,可是包括测试优化自身调整以及写好总结博客这一整套开发我仍是第一次这么完整地作下来。过程也可谓是坎坷不断。但这也正反映了《构建之法》实践这一节中对“软件工程”做业的要求:
软件工程的做业,不只仅是程序,而是要加入软件工程的要素(复杂性、易变性和其余),有价值的软件工程的做业必需要触及这两个基本要素!
不管是从测试内容的丰富性仍是效能测试的复杂性来说,这一次做业都让我从实际动手应用中收获了很多“软件开发流程”相关知识。
在效能测试模块,为了让代码的运行效率提升到理想值(100%),基本把每一条语句都尝试修改为“更好的形式”,可是结果并不尽如人意,有的只是提高了微乎其微的零点几秒,有的甚至让运行时间负增加。最后获得的13.5%的提升虽然不是很好看,可是也心服口服。
固然不管是从算法到测试再到效能提高,确定仍是有不少改进空间的,之后发现了再补充。
未完待续...