仅用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脚本使得web页面上的代码高亮显示
Apr 24 Python
介绍Python中的文档测试模块
Apr 28 Python
Python中pygame的mouse鼠标事件用法实例
Nov 11 Python
Python日期时间对象转换为字符串的实例
Jun 22 Python
Python根据欧拉角求旋转矩阵的实例
Jan 28 Python
如何用Python做一个微信机器人自动拉群
Jul 03 Python
解决python图像处理图像赋值后变为白色的问题
Jun 04 Python
python 使用三引号时容易犯的小错误
Oct 21 Python
Python实现淘宝秒杀功能的示例代码
Jan 19 Python
python pygame 愤怒的小鸟游戏示例代码
Feb 25 Python
Pytorch反向传播中的细节-计算梯度时的默认累加操作
Jun 05 Python
浅谈怎么给Python添加类型标注
Jun 08 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
为了这两部电子管收音机,买了6套全新电子管和10粒刻度盘灯泡
2021/03/02 无线电
Syphon 使用方法
2021/03/03 冲泡冲煮
PHP 翻页 实例代码
2009/08/07 PHP
在PHP中养成7个面向对象的好习惯
2010/01/28 PHP
PHP中串行化用法示例
2016/11/16 PHP
javascript 处理HTML元素必须避免使用的一种方法
2009/07/30 Javascript
高亮显示web页表格行的javascript代码
2010/11/19 Javascript
基于jQuery的简单九宫格实现代码
2012/08/09 Javascript
Jquery 表单验证类介绍与实例
2013/06/09 Javascript
js获取系统的根路径实现介绍
2013/09/08 Javascript
轻松实现js图片预览功能
2016/01/18 Javascript
jQuery实现两列等高并自适应高度
2016/12/22 Javascript
AngularJS select设置默认值的实现方法
2017/08/25 Javascript
Node.js+jade+mongodb+mongoose实现爬虫分离入库与生成静态文件的方法
2017/09/20 Javascript
NodeJS实现同步的方法
2019/03/02 NodeJs
Vue安装浏览器开发工具的步骤详解
2019/05/12 Javascript
react koa rematch 如何打造一套服务端渲染架子
2019/06/26 Javascript
JavaScript实现简单的计算器
2020/01/16 Javascript
Python中的Numeric包和Numarray包使用教程
2015/04/13 Python
Python实现快速多线程ping的方法
2015/07/15 Python
Python中asyncore异步模块的用法及实现httpclient的实例
2016/06/28 Python
PyQt5每天必学之弹出消息框
2018/04/19 Python
python如何查看微信消息撤回
2018/11/27 Python
python爬取基于m3u8协议的ts文件并合并
2019/04/26 Python
使用Python opencv实现视频与图片的相互转换
2019/07/08 Python
解决python ThreadPoolExecutor 线程池中的异常捕获问题
2020/04/08 Python
Python实现自动签到脚本功能
2020/08/20 Python
Html5剪切板功能的实现代码
2018/06/29 HTML / CSS
戴尔英国官网:Dell英国
2017/05/27 全球购物
数控技术专业毕业自荐书范文
2014/02/05 职场文书
物理专业大学生职业生涯规划书
2014/02/07 职场文书
股东授权委托书范本
2014/09/13 职场文书
教师考核鉴定意见
2015/06/05 职场文书
关于开学的感想
2015/08/10 职场文书
启迪人心的励志语录:脾气永远不要大于本事
2020/01/02 职场文书
IDEA使用SpringAssistant插件创建SpringCloud项目
2021/06/23 Java/Android