經過了前面3次課對數位輸入與回退的處理,我們的計算器已經可以基本正常地輸入數字了(輸入别太長),這次讓我們實現它的計算功能。
第一步,仍然是對計算功能的需求做一個分析。簡化起見,我們隻實現普通計算器功能:它的内部不區分算符優先級。比如你依次按下3 5*2幾個按鈕,在你按下*的時候,計算機會把3 5的結果直接計算出來,并顯示為8,沒有考慮*的優先級問題。
常見的太陽能計算器大都是這個計算邏輯
包括win10自帶的計算器,其操作邏輯也是一樣的。
計算器最典型的操作,是輸入數字1,按一個運算符按鈕( ,-,*,/),再輸入數字2,按等于按鈕,屏幕顯示結果。
我們分析一下計算器的典型操作方法。這種分析一方面有助于我們設計合理的計算模型,另一個方面,可以指定我們的測試工作。
設計編程模型中的一個非常混淆難解的問題是,把本質不同的概念用不同的變量表示,把本質相同的概念用一個變量表示。之所以它混淆難解,在于兩個概念的本質究竟是否相同,從不同的角度看,是不一樣。這個問題,在我們編程的過程中會持續存在,始終推動編程者去思考究竟一個概念的本質是什麼。這也是編程對思維的訓練效果之一。
1、按鈕事件分類
現在看我們設計計算器計算功能的編程模型。我們發現四個運算符按鈕( ,-,*,/)的本質是相同的,這個相同,指的是按下按鈕後,操作邏輯完全相同,唯一的區别是計算的方法不一樣。而運算按鈕和等于按鈕則有着本質的區别,比如前面需求中的第四點,明确表現了這種區别。所以我們的條件判斷分成兩類:
elif event in [' ', '-', '*', '/']:
#按運算符的處理
elif event in ['=']:
#按等于的處理
2、表達式
根據需求,我們看到,隻須建立一個簡單的表達式模型,它有三個成分:
我們用一個字典變量來記錄這個内部表達式,它也是一個全局變量。
expr= {'opnd1':None, 'optr': None, 'opnd2':None}
它的初始值可以為空,當計算的時候,我們用這個一個函數。
def doCalc(expr):
optr= expr['optr']
opnd1= expr['opnd1']
opnd2= expr['opnd2']
if optr is None:
result= opnd1
elif optr==' ':
result= opnd1 opnd2
elif optr=='-':
result= opnd1- opnd2
elif optr=='*':
result= opnd1* opnd2
elif optr=='/':
result= opnd1/ opnd2
return result
我們設計的init函數,使用了全局變量。可以說那是為了代碼簡化而采取的一個做法。如果有可能,我們希望函數盡量不使用全局變量。因為這樣的函數獨立性更強,我們很容易重用、也很容易轉移到其它的文件中。
這個函數的實現用了一個簡單的笨辦法:用if語句判斷運算符是什麼,根據不同的運算符做不同的計算。這樣的代碼當然不夠簡潔,但它可控性是比較好的。我們可以在編程的初期使用這種實現方法,在主體邏輯通順後,可以對它進行優化。
3、内部狀态
狀态設計是編程模型設計的一個重點,一個系統的複雜性在很大程度上就表現為内部狀态的多少。同樣的外部輸入,會得到不同的結果,這才給人以複雜的感覺——其原因就是内部狀态的不同。這裡說的複雜性與算法設計意義上常說的時間複雜性和空間複雜性不是類似的概念。從編程的角度來說,系統内部狀态的複雜性是有辦法把握的,是人力可為的;而時空複雜性雖然可以通過優化算法有一定幅度(甚至是很大幅度)的改變,但從本質上是有着不可逾越的瓶頸的。
除了剛才設置的表達式變量,我們再回顧一下前面課程中,為了控制數字的輸入,已經建立了三個變量來描述系統的内部狀态:
從需求分析看,我們至少還需增加這樣兩個内部狀态:
如果是開始一個新的數字,我們将進行一個類似的初始化操作,寫成函數是:
def initNum():
global value, decNum, sign, state
value= 0
decNum= 0
sign= 1
state= 1
我們體會一下它的用法和功能,會發現,這個函數的本質是計算器的CE按鈕。幾乎所有的計算器都有AC和CE兩個按鈕,AC代表完全的清空,恢複開機狀态(init)。CE則代表清除剛才輸入的數字。由于我們的計算器是一個極簡版,所以并沒有包括CE按鈕,但不影響這個功能的内部存在。
它的代碼就是這樣一句:
expr[pos]= sign*value
值得注意的是,value本身隻是輸入數據的絕對值,必須和sign相乘才是真正的輸入數。
代碼實現最後看一代碼下實現。當點擊運算符按鈕時,操作邏輯如下:
elif event in [' ', '-', '*', '/']:
expr[pos]= sign*value
if pos=='opnd2' and state==1:
value= doCalc(expr)
expr['opnd1']= value
expr['optr']= event
pos= 'opnd2'
state= 0
sign= 1
當輪到第二操作數并且确實輸入了數字(見需求4)則觸發運算,并把結果作為新的第一操作數。第一操作數是不觸發運算的。其它工作是記錄操作符,并标記新的位置是第二操作數,狀态置0的意思是切換到新數字模式。
當點擊等于按鈕時,操作邏輯如下:
elif event in ['=']:
expr[pos]= sign*value
value= doCalc(expr)
pos= 'opnd1'
state= 0
sign= 1
點擊等于按鈕後,操作數位置變為第一個,這對應着需求3的實現。
為什麼把結果賦值給了value呢?因為我們的屏幕顯示部分,直接用value來顯示。
為什麼必須置sign=1呢?也與顯示有關。在計算-1*8的時候沒有問題,當計算8*-1的時候,如果沒有這個設置,我們會發現一個奇怪的現象!有興趣的朋友可以自己修改代碼,看看會出現什麼現象。[呲牙]
加上了這些内部變量,再加上這兩小段事件處理代碼,我們的計算器終于可以有了一些真正的計算功能。so far so good。我們可以試試整數的加法、減法、乘法,對比一下真實的計算器,看看是否一緻。但這裡還沒有真正處理涉及小數的計算,朋友們可以試試當進行小數計算時會出現哪些問題,嘗試自己解決它們。我們把這一功能的分析留待下次課程。
最後貼一下目前為止的完整代碼。
更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!