仅用50行代码实现一个Python编写的计算器的教程


Posted in Python onApril 17, 2015

 简介

在这篇文章中,我将向大家演示怎样向一个通用计算器一样解析并计算一个四则运算表达式。当我们结束的时候,我们将得到一个可以处理诸如 1+2*-(-3+2)/5.6+3样式的表达式的计算器了。当然,你也可以将它拓展的更为强大。

我本意是想提供一个简单有趣的课程来讲解 语法分析 和 正规语法(编译原理内容)。同时,介绍一下PlyPlus,这是一个我断断续续改进了好几年的语法解析 接口。作为这个课程的附加产物,我们最后会得到完全可替代eval()的一个安全的四则运算器。

如果你想在自家的电脑上试试本文中给的例子的话,你应该先安装 PlyPlus ,使用命令pip install plyplus  。(译者注:pip是一个包管理系统,用来安装用python写的软件包,具体使用方法大家可以百度之或是google之,就不赘述了。)

本篇文章需要对python的继承使用有所了解。

语法

对于那些不懂的如何解析和正式语法工作的人而言,这里有一个快速的概览:正式语法是用来解析文本的一些不同层面的规则。每一个规则都描述了相对应的那部分输入的文本是如何组成的。

这里是一个用来展示如何解析1+2+3+4的例子:
 

Rule #1 - add IS MADE OF add + number
            OR number + number

或者用 EBNF:
 

add: add'+'number
  | number'+'number
  ;

解析器每次都会寻找add+number或者number+number,找到一个之后就会将其转换成add。基本上而言,每一个解析器的目标都在于尽可能的找到最高层次的表达式抽象。

以下是解析器的每个步骤:

number + number + number + number

    第一次转换将所有的Number变成“number”规则

[number + number] + number + number

    解析器找到了它的第一个匹配模式!

[add + number] + number

    在转换成一个模式之后,它开始寻找下一个

[add + number]
  add

这些有次序的符号变成了一个层次上的两个简单规则: number+number和add+number。这样,只需要告诉计算机如果解决这两个问题,它就能解析整个表达式。事实上,无论多长的加法序列,它都能解决! 这就是形式文法的力量。
运算符优先级

算数表达式并不仅仅是符号的线性增长,运算符创造了一个隐式的层次结构,这非常适合用形式文法来表示:

1 + 2 * 3 / 4 - 5 + 6

这相当于:

1 + (2 * 3 / 4) - 5 + 6

我们可以通过嵌套规则表示此语法中的结构:
 

add: add+mul
  | mul'+'mul
  ;
mul: mul '*; number
  | number'*'number
  ;

通过将add设为操作mul而不是number,我们就得到了乘法优先的规则。

让我们在脑海中模拟一下使用这个神奇的解析器来分析1+2*3*4的过程:

number + number * number * number
  number + [number * number] * number

    解析器不知道number+number的结果,所以这是它(解析器)的另一个选择

number + [mul * number]
  number + mul
  ???

现在我们遇到了一点困难! 解析器不知道如何处理number+mul。我们可以区分这种情况,但是如果我们继续探索下去,就会发现有很多不同的没有考虑到得可能,比如mul+number, add+number, add+add, 等等。

那么我们应该怎么做呢?

幸运的是,我们可以做一点小“把戏”:我们可以认为一个number本身是一个乘积,并且一个乘积本身是一个和!

这种思路一开始看起来有点古怪,不过它的确是有意义的:
 

add: add'+'mul
  | mul'+'mul
  | mul
  ;
mul: mul'*'number
  | number'*'number
  | number
  ;

但是如果 mul能够变成 add, 且 number能够变成 mul , 有些行的内容就变得多余了。丢弃它们,我们就得到了:
 

add: add'+'mul
  | mul
  ;
mul: mul'*'number
  | number
  ;

让我们来使用这种新的语法来模拟运行一下1+2*3*4:

number + number * number * number

    现在没有一个规则是对应number*number的了,但是解析器可以“变得有创造性”

number + [number] * number * number
  number + [mul * number] * number
  number + [mul * number]
  [number] + mul
  [mul] + mul
  [add + mul]
  add

成功了!!!

如果你觉得这个很奇妙,那么尝试着去用另一种算数表达式来模拟运行一下,然后看看表达式是如何用正确的方式来一步步解决问题的。或者等着阅读下一节中的内容,看看计算机是如何一步步运行出来的!

运行解析器

现在我们对于如何让我们的语法运作起来已经有了非常不错的想法了,那就写一个实际的语法来应用一下吧:

 

start: add;            // 这是最高层

add: add add_symbol mul | mul;

mul: mul mul_symbol number | number;

number:'[d.]+';      // 十进制数的正则表达式

mul_symbol:'*'|'/';// Match * or /

add_symbol:'+'|'-';// Match + or -

你可能想要复习一下正则表达式,但不管怎样,这个语法都非常直截了当。让我们用一个表达式来测试一下吧:
 
>>>fromplyplusimportGrammar
>>> g=Grammar("""...""")
>>>printg.parse('1+2*3-5').pretty()
start
 add
  add
   add
    mul
     number
      1
   add_symbol
    +
   mul
    mul
     number
      2
    mul_symbol
     *
    number
     3
  add_symbol
   -
  mul
   number
    5

干得漂亮!

仔细研究一下这棵树,看看解析器选择了什么层次。

如果你希望亲自运行这个解析器,并使用你自己的表达式,你只需有Python即可。安装Pip和PlyPlus之后,将上面的命令粘贴到Python内(记得将'...'替换为实际的语法哦~)。

使树成形

Plyplus会自动创建一棵树,但它并不一定是最优的。将number放入到mul和将mul放入到add非常有利于创建一个阶层,现在我们已经有了一个阶层那它们反而会成为一个负担。我们告诉Plyplus对它们加前缀去“展开”(i.e.删除)规则。

碰到一个@常常会展开一个规则,一个#则会压平它,一个?会在它有一个子结点时展开。在这种情况下,?就是我们所需要的。
 

start: add;
?add: add add_symbol mul | mul;   // Expand add if it's just a mul
?mul: mul mul_symbol number | number;// Expand mul if it's just a number
number:'[d.]+';
mul_symbol:'*'|'/';
add_symbol:'+'|'-';

在新语法下树是这样的:
 

>>> g=Grammar("""...""")
>>>printg.parse('1+2*3-5').pretty()
start
 add
  add
   number
    1
   add_symbol
    +
   mul
    number
     2
    mul_symbol
     *
    number
     3
  add_symbol
   -
  number
   5

哦,这样变得简洁多了,我敢说,它是非常好的。

括号的处理及其它特性

目前为止,我们还明显缺少一些必须的特性:括号,单元运算符(-(1+2)),及表达式中间允许存在空字符。其实这些特性都很容易就能实现,下面我们来尝试一下。

需要先引入一个重要的概念:原子。在一个原子里面(括号中及单元运算)发生的所有操作都优先于所有加法或乘法运算(包括位操作)。由于原子只是一个优先级的构造器,并无语法意义,帮我们加上"@"符号以确保在编译时它被能展开。

允许空格出现在表达式内最简单的方法就是使用这种解释方式:add SPACE add_symbol SPACE mul | mul;  但个解释结果??虑铱啥列圆睢K?校?颐切枰??lyplus总是忽略空格。

下面是完整的语法,包容了以上所述特性:
 

start: add;
?add: (add add_symbol)? mul;
?mul: (mul mul_symbol)? atom;
@atom: neg | number |'('add')';
neg:'-'atom;
number:'[d.]+';
mul_symbol:'*'|'/';
add_symbol:'+'|'-';
WHITESPACE:'[ t]+'(%ignore);

请确保理解这个语法再进入下一步:计算!

运算

现在,我们已经可以将一个表达式转化成一棵分层树了,只需要逐分支地扫描这棵树,便可得到最终结果。

我们现在要开始编写代码了,在此之前,我需要对这棵树做两点解释:

    1.每个分支都是包含如下两个属性的实例:

  •         头(head):规则的名字(例如add或者number);
  •         尾(tail):包含所有与其匹配的子规则的列表。

    2.Plyplus默认会删除不必要的标记。在本例中,'( ' ,')' 和 '-' 会被删除。但add和mul会有自己的规则,Plyplus会知道它们是必须的,从而不会被删除它们。如果你需要保留这些标记,可以手动关掉这项功能,但从我的经验来看,最好不要这样做,而是手动修改相关语法效果更佳。

言归正传,现在我们开始编写代码。我们将用一个非常简单的转换器来扫描这棵树。它会从最外面的分支开始扫描,直到到达根节点为止,而我们的工作是告诉它如何扫描。如果一切顺利的话,它将总会从最外层开始扫描!让我们看看具体的实现吧。

 

>>>importoperator as op
>>>fromplyplusimportSTransformer
 
classCalc(STransformer):
 
  def_bin_operator(self, exp):
    arg1, operator_symbol, arg2=exp.tail
 
    operator_func={'+': op.add,
             '-': op.sub,
             '*': op.mul,
             '/': op.div }[operator_symbol]
 
    returnoperator_func(arg1, arg2)
 
  number   =lambdaself, exp:float(exp.tail[0])
  neg    =lambdaself, exp:-exp.tail[0]
  __default__=lambdaself, exp: exp.tail[0]
 
  add=_bin_operator
  mul=_bin_operator

每个方法都对应一个规则。如果方法不存在的话,将调用__default__方法。我们在其中省略了start,add_symbol和mul_symbol,因为它们只会返回自己的分支。

我使用了float()来解析数字,这是个懒方法,但我也可以用解析器来实现。

为了使语句整洁,我使用了运算符模块。例如add基本上是 'lambda x,y: x+y'之类的。

OK,现在我们运行这段代码来检查一下结果。
 

>>> Calc().transform( g.parse('1 + 2 * -(-3+2) / 5.6 + 30'))
31.357142857142858

那么eval()呢?7
 

>>>eval('1 + 2 * -(-3+2) / 5.6 + 30')
31.357142857142858

成功了:)
 
最后一步:REPL

为了美观,我们把它封装到一个不错的计算器 REPL:
 

defmain():
  calc=Calc()
  whileTrue:
    try:
      s=raw_input('> ')
    exceptEOFError:
      break
    ifs=='':
      break
    tree=calc_grammar.parse(s)
    printcalc.transform(tree)

完整的代码可从这里获取:
https://github.com/erezsh/plyplus/blob/master/examples/calc.py

Python 相关文章推荐
用Python实现通过哈希算法检测图片重复的教程
Apr 02 Python
jupyter安装小结
Mar 13 Python
详解Python的Django框架中manage命令的使用与扩展
Apr 11 Python
高效测试用例组织算法pairwise之Python实现方法
Jul 19 Python
Django 对IP访问频率进行限制的例子
Aug 30 Python
python集合的创建、添加及删除操作示例
Oct 08 Python
Python连接SQLite数据库并进行增册改查操作方法详解
Feb 18 Python
python实现简单俄罗斯方块
Mar 13 Python
Python+OpenCV图像处理——实现轮廓发现
Oct 23 Python
解决pytorch下出现multi-target not supported at的一种可能原因
Feb 06 Python
python实现调用摄像头并拍照发邮箱
Apr 27 Python
matplotlib如何设置坐标轴刻度的个数及标签的方法总结
Jun 11 Python
python字典get()方法用法分析
Apr 17 #Python
详解Python中__str__和__repr__方法的区别
Apr 17 #Python
使用Python设置tmpfs来加速项目的教程
Apr 17 #Python
在Python上基于Markov链生成伪随机文本的教程
Apr 17 #Python
基于scrapy实现的简单蜘蛛采集程序
Apr 17 #Python
在Python的Django框架中实现Hacker News的一些功能
Apr 17 #Python
由Python运算π的值深入Python中科学计算的实现
Apr 17 #Python
You might like
php入门学习知识点六 PHP文件的读写操作代码
2011/07/14 PHP
调整PHP的性能
2013/10/30 PHP
关于PHP 如何用 curl 读取 HTTP chunked 数据
2016/02/26 PHP
js或css文件后面跟参数的原因说明
2010/01/09 Javascript
匹配任意字符的正则表达式写法
2010/04/29 Javascript
基于JQuery的简单实现折叠菜单代码
2010/09/15 Javascript
TypeScript 中接口详解
2015/06/19 Javascript
Javascript之面向对象--接口
2016/12/02 Javascript
微信小程序 使用腾讯地图SDK详解及实现步骤
2017/02/28 Javascript
详解Vue-cli webpack移动端自动化构建rem问题
2018/04/07 Javascript
JavaScript基础教程之如何实现一个简单的promise
2018/09/11 Javascript
js设计模式之单例模式原理与用法详解
2019/08/15 Javascript
Node 模块原理与用法详解
2020/05/13 Javascript
vue同个按钮控制展开和折叠同个事件操作
2020/07/29 Javascript
[46:59]完美世界DOTA2联赛PWL S2 GXR vs Ink 第二场 11.19
2020/11/20 DOTA
Python算法应用实战之栈详解
2017/02/04 Python
浅谈机器学习需要的了解的十大算法
2017/12/15 Python
python八皇后问题的解决方法
2018/09/27 Python
使用11行Python代码盗取了室友的U盘内容
2018/10/23 Python
Python SMTP发送邮件遇到的一些问题及解决办法
2018/10/24 Python
对python中的控制条件、循环和跳出详解
2019/06/24 Python
提升Python效率之使用循环机制代替递归函数
2019/07/23 Python
Python3从零开始搭建一个语音对话机器人的实现
2019/08/23 Python
Python使用itchat模块实现群聊转发,自动回复功能示例
2019/08/26 Python
Django中的session用法详解
2020/03/09 Python
python安装后的目录在哪里
2020/06/21 Python
Python3中FuzzyWuzzy库实例用法
2020/11/18 Python
Python机器学习工具scikit-learn的使用笔记
2021/01/28 Python
pycharm 的Structure界面设置操作
2021/02/05 Python
简述进程的启动、终止的方式以及如何进行进程的查看
2013/07/12 面试题
劲霸男装广告词改编版
2014/03/21 职场文书
社保缴纳证明申请书
2014/11/03 职场文书
交通事故被告代理词
2015/05/23 职场文书
redis三种高可用方式部署的实现
2021/05/11 Redis
Vue实现动态查询规则生成组件
2021/05/27 Vue.js
Python办公自动化之教你如何用Python将任意文件转为PDF格式
2021/06/28 Python