经过了前面3次课对数位输入与回退的处理,我们的计算器已经可以基本正常地输入数字了(输入别太长),这次让我们实现它的计算功能。
![324ac2c329243d8c071858456a4ad8a2.png](https://i-blog.csdnimg.cn/blog_migrate/608329890c015a7e1aeeee27f96b9e5a.jpeg)
计算功能需求分析
第一步,仍然是对计算功能的需求做一个分析。简化起见,我们只实现普通计算器功能:它的内部不区分算符优先级。比如你依次按下3+5*2几个按钮,在你按下*的时候,计算机会把3+5的结果直接计算出来,并显示为8,没有考虑*的优先级问题。
![ed5e03b4832f310551f90b4d9fec7f39.png](https://i-blog.csdnimg.cn/blog_migrate/a0e715ad63a4452d6f8e5efce2663d83.jpeg)
常见的太阳能计算器大都是这个计算逻辑
包括win10自带的计算器,其操作逻辑也是一样的。
![0f8b79240b5cd448bd6dba64b01bd63a.gif](https://i-blog.csdnimg.cn/blog_migrate/feb054b4ebe023bff2f2d1cdaf255cd6.gif)
计算器最典型的操作,是输入数字1,按一个运算符按钮(+,-,*,/),再输入数字2,按等于按钮,屏幕显示结果。
我们分析一下计算器的典型操作方法。这种分析一方面有助于我们设计合理的计算模型,另一个方面,可以指定我们的测试工作。
- 第一,接续运算,就如同我们上面的动画演示一样,数字+运算符+数字+运算符+数字,可以多个,最后再按等于按钮,显示结果。其实,这种情况中,每次按运算符按钮时,中间的结果都被计算并显示出来,等于按钮只是做最后一步的运算。
- 第二,连续等于,大概经常用于无聊又无事可做的时候:连续按等于按钮,计算器就会持续用同一个规则进行计算。如下:
![d6906223460ca468153beadb5465351f.gif](https://i-blog.csdnimg.cn/blog_migrate/028b742fa9619ee250e4a421abcb6019.gif)
- 第三,在连续等于的操作后,如果我们输入一个数字,立即又按等于按钮,我们发现:这个新输入的数字被当作第一操作数来处理。以前门的操作为例:72-1-1-1-1之后,如果我们按数字5,再连续按等于,我们发现结果将是5,4,3,2,1,0,-1这个顺序变化。即运算符(-)和第二操作数(1)均保持不变。
- 第四,在接续运算的操作中,运算符按钮的作用从含义上等价于先按了等于按钮再按按运算符按钮;但在连续等于的操作中,这里如果按运算符按钮,却没有连续计算的效果。经过思考和比较,我们发现,两者的区别是在两次按运算符之间是否输入过数字。
- 第五,在输入运算符后,如果不输入数字,直接按等于,是可以进行运算的。这种情况下,第一、二操作数是相同。即按等于按钮的时候,当前显示屏上的数字被理解为第二操作数。比如我们想计算9+9的结果,我们可以这样按键9+=。
- 第六,按下运算符按钮或者等于按钮之后,继续按数字键或小数点键不会继续改变这个数字,而是重新开始输入数字。这一现象提示我们,这里可能存在一个内部状态的改变。
编程模型设计
设计编程模型中的一个非常混淆难解的问题是,把本质不同的概念用不同的变量表示,把本质相同的概念用一个变量表示。之所以它混淆难解,在于两个概念的本质究竟是否相同,从不同的角度看,是不一样。这个问题,在我们编程的过程中会持续存在,始终推动编程者去思考究竟一个概念的本质是什么。这也是编程对思维的训练效果之一。
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、内部状态
状态设计是编程模型设计的一个重点,一个系统的复杂性在很大程度上就表现为内部状态的多少。同样的外部输入,会得到不同的结果,这才给人以复杂的感觉——其原因就是内部状态的不同。这里说的复杂性与算法设计意义上常说的时间复杂性和空间复杂性不是类似的概念。从编程的角度来说,系统内部状态的复杂性是有办法把握的,是人力可为的;而时空复杂性虽然可以通过优化算法有一定幅度(甚至是很大幅度)的改变,但从本质上是有着不可逾越的瓶颈的。
除了刚才设置的表达式变量,我们再回顾一下前面课程中,为了控制数字的输入,已经建立了三个变量来描述系统的内部状态:
- value:数值
- sign:正负号
- decNum:小数位
从需求分析看,我们至少还需增加这样两个内部状态:
- 1、当前输入数字按钮时,是重新开始一个新的数字,还是继续屏幕上的数字。我们可以设置一个state(状态)变量,它有两个值:0代表开始一个新的数字,1代表继续输入。
如果是开始一个新的数字,我们将进行一个类似的初始化操作,写成函数是:
def initNum(): global value, decNum, sign, state value= 0 decNum= 0 sign= 1 state= 1
我们体会一下它的用法和功能,会发现,这个函数的本质是计算器的CE按钮。几乎所有的计算器都有AC和CE两个按钮,AC代表完全的清空,恢复开机状态(init)。CE则代表清除刚才输入的数字。由于我们的计算器是一个极简版,所以并没有包括CE按钮,但不影响这个功能的内部存在。
- 2、当前输入的这个数字,是第一操作数还是第二操作数。我们设置变量pos(位置),它有两个值:分别是opnd1和opnd2,分别对应着两个操作数。这样定值的好处是我们可以直接用这个变量把数字输入表达式对应的位置中。
它的代码就是这样一句:
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。我们可以试试整数的加法、减法、乘法,对比一下真实的计算器,看看是否一致。但这里还没有真正处理涉及小数的计算,朋友们可以试试当进行小数计算时会出现哪些问题,尝试自己解决它们。我们把这一功能的分析留待下次课程。
最后贴一下目前为止的完整代码。
![0667e8d853e3ae05c7aa3afa14f4281c.png](https://i-blog.csdnimg.cn/blog_migrate/0ccc37db0c6f55cd6c219abcf071b740.jpeg)