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

 简介

在这篇文章中,我将向大家演示怎样向一个通用计算器一样解析并计算一个四则运算表达式。当我们结束的时候,我们将得到一个可以处理诸如 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;  但个解释结果啰嗦且可读性差。所有,我们需要令Plyplus总是忽略空格。

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

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

时间: 2015-04-16

python正则表达式之作业计算器

作业:计算器开发 实现加减乘除及拓号优先级解析 用户输入 1 - 2 * ( (60-30 +(-40/5) * (9-2*5/3 + 7 /3*99/4*2998 +10 * 568/14 )) - (-4*3)/ (16-3*2) )等类似公式后,必须自己解析里面的(),+,-,*,/符号和公式,运算后得出结果,结果必须与真实的计算器所得出的结果一致 一.说明: 有一点bug就是不能计算幂次方,如:'6**6'会报错 该计算器思路: 1.没用使用递归,先找出并计算所有括号里的公式,再计算乘除

Python设计实现的计算器功能完整实例

本文实例讲述了Python设计实现的计算器功能.分享给大家供大家参考,具体如下: 通过利用PYTHON 设计处理计算器的功能如: 1 - 2 * ( (60-30 +(-40/5) * (9-2*5/3 + 7 /3*99/4*2998 +10 * 568/14 ))- (-4*3)/(16-3*2)) 我的处理计算基本思路是: 解题思路是,需要优先处理内层括号运算--外层括号运算--先乘除后加减的原则: 1.正则处理用户输入的字符串,然后对其进行判断,判断计算公式是否有括号,有就先将计算公式进

Python开发的实用计算器完整实例

本文实例讲述了Python开发的实用计算器.分享给大家供大家参考,具体如下: 实现功能:图形界面PyQt,输入框,+,-,*,/ :乘方 ,开方 ,取余,清零. 1. Python代码: #!/usr/bin/env python # -*- coding: utf-8 -*- ''' Author : Mr.LiuYC Created on 2014-09-30 E-Mail : liuyanchen0725@gmail.com Introduction: 简易计算器 实现图形界面PyQt,输

python 简易计算器程序,代码就几行

代码: 复制代码 代码如下: import os while True: dynamic = input('输入计算表达式:') if dynamic != 'cls': try: result = eval(dynamic) print('计算结果:'+str(result)) except: print('计算表达式输入有误!') else: command = 'cls' os.system(command)

Python 正则表达式实现计算器功能

需求: 用户输入运算表达式,终端显示计算结果 代码: # !/usr/bin/env/ python3 # -*- coding: utf-8 -*- """用户输入计算表达式,显示计算结果""" __author__ = 'Jack' import re bracket = re.compile(r'\([^()]+\)') # 寻找最内层括号规则 mul = re.compile(r'(\d+\.?\d*\*-\d+\.?\d*)|(\d+\

基于wxpython开发的简单gui计算器实例

本文实例讲述了基于wxpython开发的简单gui计算器.分享给大家供大家参考.具体如下: # wxCalc1 a simple GUI calculator using wxPython # created with the Boa Constructor which generates all the GUI components # all I had to do is add some code for each button click event # Boa free from: h

Python实现的科学计算器功能示例

本文实例讲述了Python实现的科学计算器功能.分享给大家供大家参考,具体如下: import wx import re import math # begin wxGlade: extracode # end wxGlade ans=0 ts="" class MyFrame(wx.Frame): def __init__(self, *args, **kwds): # begin wxGlade: MyFrame.__init__ kwds["style"] =

python概率计算器实例分析

本文实例讲述了python概率计算器实现方法.分享给大家供大家参考.具体实现方法如下: from random import randrange #randrange form random module def calc_prob(strengths): """A function that receives an array of two numbers indicating the strength of each party and returns the winne

python3.5 + PyQt5 +Eric6 实现的一个计算器代码

目前可以实现简单的计算.计算前请重置,设计的时候默认数字是0,学了半天就做出来个这么个结果,bug不少. python3.5 + PyQt5 +Eric6 在windows7 32位系统可以完美运行 计算器,简单学了半天就画个图实现的存在bug,部分按钮还未实现,后续优化. 代码结构如图: jisuan.py import re #匹配整数或小数的乘除法,包括了开头存在减号的情况 mul_div=re.compile("(-?\d+)(\.\d+)?(\*|/)(-?\d+)(\.\d+)?&q

python3.5仿微软计算器程序

本文实例为大家分享了python3.5仿微软计算器的具体代码,供大家参考,具体内容如下 from tkinter import * from math import * root = Tk() root.title("计算器") root.geometry("1200x260+0+0") def come(event): event.widget["background"]= "orange"#event的widget方法,恩

Python实现简单的四则运算计算器

一.算法 1.算法的主要思想就是将一个中缀表达式(Infix expression)转换成便于处理的后缀表达式(Postfix expression),然后借助于栈这个简单的数据结构,计算出表达式的结果. 2.关于如何讲普通的表达式转换成后缀表达式,以及如何处理后缀表达式并计算出结果的具体算法描述不在此叙述了,书上有详细的说明. 二.简易计算器 使用说明 使用该计算器类的简单示例如下: # usage c = Calculator() print('result: {:f}'.formart(c

C#实现的简单整数四则运算计算器功能示例

本文实例讲述了C#实现的简单整数四则运算计算器功能.分享给大家供大家参考,具体如下: 运行效果图如下: 具体代码如下: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; namespace 计算器 { public pa

JavaScript实现简单的四则运算计算器完整实例

本文实例讲述了JavaScript实现简单的四则运算计算器.分享给大家供大家参考,具体如下: 运行效果图如下: 完整实例代码如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>computer</title> <script> function compute(){ str1=Number

PHP实现的简单四则运算计算器功能示例

本文实例讲述了PHP实现的简单四则运算计算器功能.分享给大家供大家参考,具体如下: php实现一个简单的四则运算计算器(还不支持括号的优先级).利用栈这种数据结构来计算表达式很赞. 这里可以使用栈的结构,由于php的数组"天然"就有栈的特性,这里直接就利用了数组.当然可以使用栈结构写,道理一样的. 前辈(波兰一位科学家)在计算带有括号的四则表达式,利用逆波兰算法(后缀表达法).简直神了!!其实代码code并不难,难的是算法的指导,要先理解算法,才能编码. <?php $num_a

JS实现的简单四则运算计算器功能示例

本文实例讲述了JS实现的简单四则运算计算器功能.分享给大家供大家参考,具体如下: 先来看看运行效果: 具体代码如下: <!DOCTYPE html> <html> <meta name="content-type" content="text/html; charset=UTF-8"> <head> <title>www.jb51.net 计算器 Calculator</title> <!

JS实现的加减乘除四则运算计算器示例

本文实例讲述了JS实现超级简易的加减乘除四则运算计算器.分享给大家供大家参考,具体如下: 先来看看运行效果: 具体代码如下: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>简单的计算器(www.jb51.net)</title> <style> body{ margin: 0; } .tab{ border: 3px solid

Python实现简单的语音识别系统

最近认识了一个做Python语音识别的朋友,聊天时候说到,未来五到十年,Python人工智能会在国内掀起一股狂潮,对各种应用的冲击,不下于淘宝对实体经济的冲击.在本地(江苏某三线城市)做这一行,短期可能显不出效果,但从长远来看,绝对是一个高明的选择.朋友老家山东的,毕业来这里创业,也是十分有想法啊. 将AI课上学习的知识进行简单的整理,可以识别简单的0-9的单个语音.基本方法就是利用库函数提取mfcc,然后计算误差矩阵,再利用动态规划计算累积矩阵.并且限制了匹配路径的范围.具体的技术网上很多,不

Python实现简单求解给定整数的质因数算法示例

本文实例讲述了Python实现简单求解给定整数的质因数算法.分享给大家供大家参考,具体如下: 接着做题遇到求解质因数分解的问题,思想很简单,就是需要遍历从1到该整数本身,并且判断当数字为质数时加入列表最后输出即可,求解这样的一个正整数的质因数分解,关键在于理解,每次得到一个质因数之后需要更新整数为:原始整数除以这个质因数的值,循环直至原始整数的值小于2终止,输出结果即可,实现如下: #!usr/bin/env python #encoding:utf-8 ''''' __Author__:沂水寒

java图形化界面实现简单混合运算计算器的示例代码

写了好几天了终于写完了这个四则运算计算器,总代码放在后面 截图如下: 首先是布局都比较简单,最上面的一个框是总的输出框, 第二个框是每次输入的数字显示在框内, 对于每一个按钮都增加监听器, 对于数字按钮:当长度大于8的 或者等号已经出现之后就不再处理按钮事件 if(e.getSource().equals(button1)) { s=numberText.getText(); //数字长度大于8或者等号已出现 if(s.length()>8 || equalbook == 1) { } else

Python脚本简单实现打开默认浏览器登录人人和打开QQ的方法

本文实例讲述了Python脚本简单实现打开默认浏览器登录人人和打开QQ的方法.分享给大家供大家参考,具体如下: 每天打开电脑第一件事应该就是打开人人刷一下,然后登上QQ.每次都这样很麻烦,于是写了一个脚本,每次双击即可自动完成这两个工作. 注意:需要在人人登录时选择"下次自动登录",QQ也要选择自动登录.其实感觉这些设置都是没必要的,都可以用脚本完成,但是本人比较水,就偷了懒,没有去查资料. 代码如下: todo.pyw: import webbrowser import os web