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%的提升雖然不是很好看,可是也心服口服。
固然不管是從算法到測試再到效能提高,確定仍是有不少改進空間的,之後發現了再補充。
未完待續...