仅用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中使用enumerate函数遍历元素实例
Jun 16 Python
Python正则表达式匹配HTML页面编码
Apr 08 Python
举例讲解Linux系统下Python调用系统Shell的方法
Nov 07 Python
Python判断值是否在list或set中的性能对比分析
Apr 16 Python
python 实现tar文件压缩解压的实例详解
Aug 20 Python
Python数据结构与算法之图的广度优先与深度优先搜索算法示例
Dec 14 Python
scrapy爬虫实例分享
Dec 28 Python
查看django版本的方法分享
May 14 Python
使用python的pandas为你的股票绘制趋势图
Jun 26 Python
python 绘制拟合曲线并加指定点标识的实现
Jul 10 Python
快速查找Python安装路径方法
Feb 06 Python
用python介绍4种常用的单链表翻转的方法小结
Feb 24 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把空格、换行符、中文逗号等替换成英文逗号的正则表达式
2014/05/04 PHP
微信公众平台网页授权获取用户基本信息中授权回调域名设置的变动
2014/10/21 PHP
php实现过滤字符串中的中文和数字实例
2015/07/29 PHP
php实现的XML操作(读取)封装类完整实例
2017/02/23 PHP
PHP接口类(interface)的定义、特点和应用示例
2020/05/18 PHP
深入理解JavaScript系列(4) 立即调用的函数表达式
2012/01/15 Javascript
jquery ajax例子返回值详解
2012/09/11 Javascript
jquery $.each() 使用小探
2013/08/23 Javascript
jQuery 设置 CSS 属性示例介绍
2014/01/16 Javascript
JavaScript修改浏览器tab标题小技巧
2015/01/06 Javascript
使用jquery提交form表单并自定义action的实现代码
2016/05/25 Javascript
Javascript之Math对象详解
2016/06/07 Javascript
vue组件发布到npm简单步骤
2017/11/30 Javascript
JS实现的Object数组去重功能示例【数组成员为Object对象】
2019/02/01 Javascript
vue+echarts实现可拖动节点的折线图(支持拖动方向和上下限的设置)
2019/04/12 Javascript
vue+element实现表格新增、编辑、删除功能
2019/05/28 Javascript
JavaScript工具库之Lodash详解
2019/06/15 Javascript
Python数据分析中Groupby用法之通过字典或Series进行分组的实例
2017/12/08 Python
python3.5绘制随机漫步图
2018/08/27 Python
python程序 创建多线程过程详解
2019/09/23 Python
python 使用递归回溯完美解决八皇后的问题
2020/02/26 Python
python 基于opencv实现图像增强
2020/12/23 Python
Python实现给PDF添加水印的方法
2021/01/25 Python
Python tkinter之ComboBox(下拉框)的使用简介
2021/02/05 Python
python抢购软件/插件/脚本附完整源码
2021/03/04 Python
New Balance俄罗斯官方网上商店:购买运动鞋
2020/03/02 全球购物
机械工程师求职自我评价
2013/09/23 职场文书
企划主管岗位职责
2013/12/12 职场文书
大学生的应聘自我评价
2013/12/13 职场文书
文化活动实施方案
2014/03/28 职场文书
读书伴我成长演讲稿
2014/05/07 职场文书
中华美德颂演讲稿
2014/05/20 职场文书
助人为乐道德模范事迹材料
2014/08/16 职场文书
员工激励培训演讲稿
2014/09/16 职场文书
财务工作失误检讨书
2015/02/19 职场文书
总结三种用 Python 作为小程序后端的方式
2022/05/02 Python