軟工第2次做業-四則運算器

Github項目地址

https://github.com/wapleeeeee/Arithmetic-operationhtml


PSP

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

項目要求

  • 參與運算的操做數(operands)除了100之內的整數之外,還要支持真分數的四則運算。操做數必須隨機生成。
  • 運算符(operators)爲 +, −, ×, ÷ 運算符的種類和順序必須隨機生成。
  • 要求能處理用戶的輸入,並判斷對錯,打分統計正確率。
  • 使用 -n 參數控制生成題目的個數。
附加要求
  • 支持帶括號的多元複合運算
  • 運算符個數隨機生成(考慮小學生運算複雜度,範圍在2~10)

解題思路

這個題目最開始是在課堂上何老師提出引發你們的思考,一開始我並無意識到這個題目的複雜性。這個題目能夠被劃分爲如下三個問題:python

  1. 列出隨機的四則運算表達式。
  2. 計算所列出四則運算的結果。
  3. 接受用戶輸入並比較結果是否正確,並在命令行中表現出來。

如何解決

問題1

  • 隨機操做數、隨機運算符、隨機括號、隨機長度等隨機變量能夠利用python自帶隨機函數取得。
  • 須要考慮除數及分母爲0時的狀況,此時表達式不成立。
  • 須要考慮隨機生成括號的位置是否有意義的狀況。例如:
expression:  (1+2+5)-(3*4)=

問題2

求通常四則算數表達式的結果通常採用轉化爲逆波蘭表達式
該種方法通常思路爲:git

  1. 將中綴表達式轉化成後綴表達式。
  2. 經過棧計算後綴表達式的值。
    其中須要考慮運算符優先級問題以及棧的結構。查閱了網上的資料,參考了dragondove的博客以及yichudu的博客。其中都有關於該算法的的具體描述。

問題3

判斷用戶輸入狀況只需接受用戶輸入比較統計得分便可。因爲要求採用命令行界面完成,該部分主要須要控制及美化命令行界面。github

測試部分

因爲四則運算的規則繁雜,隨機生成的算式須要判斷各類狀況的產生,也就無形之中給測試部分增添了很大壓力。看過了《構建之法》第二章的全部測試部分以後,利用其中單元測試的部分對項目進行了統一的測試。詳細狀況見後文測試部分。算法


設計實現

1、整體設計

image

2、具體程序設計

主程序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))

運行效果

運行結果以下所示:
imagedom


單元測試

《構建之法》第二章中詳細說起了好的單元測試的標準。ide

  • 單元測試應該在最基本的功能/參數上檢驗程序的正確性。
  • 單元測試必須由最熟悉代碼的人來寫。
  • 單元測試事後,機器狀態保持不變。
  • 單元測試要快。
  • 單元測試應該產生可重複、一致的結果。
  • 獨立性-單元測試的運行/經過/失敗不依賴於別的測試,能夠人爲構造數據,以保持測試的獨立性。
  • 單元測試應該覆蓋全部代碼路徑。

這些思想再加上該項目中表達式內容和形式的變幻無窮,構建出一套合適的測試體系成爲了這個項目中不可或缺的重要部分。所以我對錶達式類中每個函數詳細地構造出一套測試方法。函數

函數名 輸入 輸出 測試方法 備註
getEquation void string finalstring:表達式函數 使用python自帶eval函數檢驗式子合法性 因爲該函數包含insertBracket而且輸出相同,只須要測試該函數便可覆蓋。
getAnswer string equ:表達式 Fraction answer:計算結果 給出不一樣狀況的表達式,測試輸入輸出結果是否相同。
change_list list 中綴表達式 list 後綴表達式 給出特定中綴表達式,測試可否轉化成所期待的後綴表達式。 包含於getAnswer,但須要單獨測試。
calculate list 後綴表達式 Fraction answer:計算結果 輸入指定後綴表達式,匹配結果是否相同。 包含於getAnswer,但須要單獨測試。

getEquation函數

測試代碼以下:

#對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。
image

  • 經過測試

getAnswer函數

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

單獨運行測試獲得結果以下:
image


change_list函數

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, '÷']

運行結果以下:
image


calculate函數

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)

單元測試運行效果以下:
image


集成測試

將以上五個單元測試疊在一塊兒測試,而且增長了錯誤狀況的判斷,讓方法魯棒性更強。測試結果以下:
image


效能分析與改進

首先考慮到用戶輸入會影響效能分析中的時間因素,去掉了主函數中接受用戶輸入並比較的部分,直接改爲由代碼隨機生成算式而後計算結果。

python中的效能分析工具這是一個很是好用的python性能分析工具的介紹,包含了line_profiler(時間分析)以及memory_profiler(內存分析)等python包。

開始

首先測試10W條
統計出五個函數的運行時間及細節以下:

getEquation函數

10W次運行時間15.3507s(33.73%)
image

insertBracket函數

10W次運行時間3.30584s(9.26%)
image

getAnswer函數

10W次運行時間20.3809s(25.7%)(函數內)
image

change_list函數

10W次運行時間4.4608s(12.68%)
image

calculate函數

10W次運行時間6.73093s(18.85%)
image

共計耗時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

分析上表能夠看出,效率不高的幾條語句大體能夠分爲如下三類:

  1. 調用random.randint獲得隨機值。
  2. 一條參數中使用不少方法嵌套。
  3. 調用類中的其餘函數。

改進
  • python中random.randint是一個產生隨機數的高效方法,自己沒有多少可以替換的函數。因爲咱們只須要random中的randint函數,因而考慮替換import方式:
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優化的百分比並很少,不考慮在這個上面作文章了。

再看剛剛修改的函數調用部分,雖然提升了很多,但跟其餘語句比起來依然慘淡很多:
image

進一步分析發現每一句話都調用了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%的提升雖然不是很好看,可是也心服口服。

固然不管是從算法到測試再到效能提高,確定仍是有不少改進空間的,之後發現了再補充。

未完待續...

相關文章
相關標籤/搜索