用Python写了个计算器,才知道“简单”两个字有多难

学编程这几年,我做过不少东西,但真让我体会到“把复杂留给自己,把简单交给用户”这句话分量的,是一个小小的计算器。

起因很普通。有段时间我经常要算一些工程上的数,开电脑、找计算器、切窗口,烦。我就想,干脆自己写一个,反正Python写界面也不难。当时想得很美:一个输入框,几个按钮,按一按出结果,半小时的事。后来才知道,这个“半小时的事”,我断断续续折腾了好几个晚上。

 

第一版:能算就行

第一版我用的是tkinter,Python自带的,不用装东西。界面就是上面一个Entry显示输入,下面一排按钮,0到9、加减乘除、等号、清空。按钮回调里拼字符串,等号一按,用eval()直接算。

代码不到五十行,跑起来还真能用。我按了个1+2,回车,出来个3,心里挺美。那一刻觉得Python是真方便,换个语言光搭界面就得写半天。

但美了没几分钟。我随手按了个1/0,程序直接抛出异常,界面卡在那儿不动了。再按个1+,eval()报语法错误,又是一堆红字。还有更离谱的,我在输入框里敲了import os,它居然真去执行了——eval()这东西,你给它什么它就执行什么,根本不挑。

这时候我才意识到,eval()看着省事,其实是个坑。它把“算数”和“执行代码”混为一谈了,用户能输入什么,你根本管不住。我把它换掉了,自己写了个解析器。

第二版:自己动手算

不用eval(),就得自己把1+2*3这种字符串拆开,搞清楚先算乘除后算加减,还得处理括号。这块我一开始想偷懒,用split()按运算符切开,结果一遇到12+3这种多位数就散了架,-5这种负数也处理不了。

后来老老实实写了个简单的递归下降解析器。思路不复杂:先写个分词函数,把字符串切成数字和运算符;再按优先级写解析函数,加减一层、乘除一层、括号一层,一层层往下递归。写的时候挺绕,尤其处理负号和括号嵌套那块,改了好几遍才顺。

text
def parse_expr(tokens):
    # 先处理加减
    ...
def parse_term(tokens):
    # 再处理乘除
    ...
def parse_factor(tokens):
    # 最后处理数字和括号
    ...

写完跑通那一刻,我盯着屏幕乐了半天。1+2*3出来7,(1+2)*3出来9,-5+3出来-2,全对。那种感觉跟自己亲手把一台机器拆开、再装回去、然后它真转起来了一样,特别踏实。

第三版:把错误当回事

解析器写好了,但用户不会按你的规矩来。除以零、括号不配对、连按两个运算符、输入一堆乱码,这些都得管。我给每个可能出错的地方加了try...except,捕获到异常就在界面上显示一句人话,比如“除数不能为零”“表达式不完整”,而不是让程序崩掉或者弹一堆英文报错。

这中间还踩了个坑。一开始我把异常处理写在最外层,结果一出错整个计算流程就断了,用户得从头再输。后来改成在解析的每一层各自处理,出错就往上抛一个自定义的异常,最外层统一接住显示,输入框里的内容还能保留,用户改一改接着算。这个小改动让我第一次真切体会到,错误处理不是补丁,是设计的一部分。

最后:简单是打磨出来的

这个计算器最后不到两百行,功能也朴素:四则运算、括号、小数、正负号,加个清空和退格。但就这两百行,我改了不知道多少遍。界面按钮对齐调了半天,键盘输入支持补上,连续运算的边界情况一个个试。

做完那天我把它发给朋友用,他按了几下说“挺好用的”,就这三个字,我高兴了一晚上。

回头想,这个计算器教会我的,比看上去多。eval()的教训让我知道,图省事的代码迟早要还债;手写解析器让我把编译原理课上那些“词法分析”“递归下降”真正摸了一遍;异常处理让我明白,一个程序好不好用,一半看功能,一半看它出错的时候体不体面。

Python的好,在于它让我这种半路出家的人,也能在几个晚上里,把一个想法从头到尾做成一个能用的东西。界面、逻辑、容错,一样不少,但每一样都不至于把人卡死。做完之后那种“这是我亲手攒出来的”喜悦,很实在。

现在我电脑里还留着这个计算器,虽然系统自带的更方便,但我偶尔还是会打开它,按两下。看着那个自己一点点磨出来的小窗口,心里总有点小小的得意。