极简四则运算解释器

news2024/9/25 1:19:37

前言: 这是最近完成的一个小的 demo,一个极简四则运算解释器。前面,已经基于这个想法发了两篇博客了:
四则运算和二叉树
简单四则运算语法树可视化
然后,前两天也就完成了这个总体的 demo 程序。本来整个程序的思路大致上有了,但是卡在了生成 AST 上了,因为不知道如何生成表达式树的二叉树,这个问题困扰了我很久。最后我参考了下面参考资料的第一篇博客的做法,不过它的树的节点我感觉有点简单了,而且最后底层叶子节点为 tree=tree(tree, "+", 0),感觉莫名其妙的,虽然这样也是可以的。不过,我最终是定义了两个节点来表示这个,一个 BinOp 来表示二元运算节点,另一个 Constant 来表示常量节点。正好元旦到了,我也能抽出来时间了,正好写一篇博客记录一下。

演示

这里来简单放几个程序的运行截图,给大家看一下演示的效果。

计算功能演示

这里先展示了程序的帮助信息,然后是几个简单的四则运算测试,看起来是没问题了(我可不敢保证,程序没有bug!)。
在这里插入图片描述

输出 tokens

在这里插入图片描述

输出 AST

这个格式化的 JSON 信息太长了,不利于直接看到。我们将它渲染出来看最后生成的树形图(方法见前两个博客)。保存下面这个 JSON 在一个文件中,这里我叫做 demo.json,然后执行如下命令:pytm-cli -d LR -i demo.json -o demo.html,然后再浏览器打开生成的 html 文件。
在这里插入图片描述

在这里插入图片描述

介绍

代码

所有的代码都在这里了,只需要一个文件 my_eval.py,想要运行的话,复制、粘贴,然后按照演示的步骤执行即可。

Node、BinOp、Constan 是用来表示节点的类.
Calculator 中 lexizer 方法是进行分词的,本来我是打算使用正则的,如果你看过我前面的博客的话,可以发现我是用的正则来分词的(因为 Python 的官方文档正则表达式中有一个简易的分词程序)。不过我看其他人都是手写的分词,所以我也这样做了,不过感觉并不是很好,很繁琐,而且容易出错。
parse 方法是进行解析的,主要是解析表达式的结构,判断是否符合四则运算的文法,最终生成表达式树(它的 AST)。

"""
Grammar

G -> E
E -> T E'
E' -> '+' T E' | '-' T E' | ɛ
T -> F T'
T' -> '*' F T' | '/' F T' | ɛ
F -> '(' E ')' | num | name

"""

import json
import argparse


class Node:
    """
    简单的抽象语法树节点,定义一些需要使用到的具有层次结构的节点
    """

    def eval(self) -> float: ...   # 节点的计算方法
    def visit(self): ...           # 节点的访问方法


class BinOp(Node):
    """
    BinOp Node
    """

    def __init__(self, left, op, right) -> None:
        self.left = left
        self.op = op
        self.right = right

    def eval(self) -> float:
        if self.op == "+":
            return self.left.eval() + self.right.eval()
        if self.op == "-":
            return self.left.eval() - self.right.eval()
        if self.op == "*":
            return self.left.eval() * self.right.eval()
        if self.op == "/":
            return self.left.eval() / self.right.eval()
        return 0

    def visit(self):
        """
        遍历树的各个节点,并生成 JSON 表示
        """

        return {
            "name": "BinOp",
            "children": [
                self.left.visit(),
                {
                    "name": "OP",
                    "children": [
                        {
                            "name": self.op
                        }
                    ]
                },
                self.right.visit()
            ]
        }


class Constant(Node):
    """
    Constant Node
    """

    def __init__(self, value) -> None:
        self.value = value

    def eval(self) -> float:
        return self.value

    def visit(self):
        return {
            "name": "NUMBER",
            "children": [
                {
                    "name": str(self.value)  # 转成字符是因为渲染成图像时,需要该字段为 str
                }
            ]
        }


class Calculator:
    """
    Simple Expression Parser
    """

    def __init__(self, expr) -> None:
        self.expr = expr           # 输入的表达式
        self.parse_end = False     # 解析是否结束,默认未结束
        self.toks = []             # 解析的 tokens
        self.index = 0             # 解析的下标

    def lexizer(self):
        """
        分词
        """
        index = 0
        while index < len(self.expr):
            ch = self.expr[index]
            if ch in [" ", "\r", "\n"]:
                index += 1
                continue
            if '0' <= ch <= '9':
                num_str = ch
                index += 1
                while index < len(self.expr):
                    n = self.expr[index]
                    if '0' <= n <= '9':
                        if ch == '0':
                            raise Exception("Invalid number!")
                        num_str = n
                        index += 1
                        continue
                    break
                self.toks.append({
                    "kind": "INT",
                    "value": int(num_str)
                })
            elif ch in ['+', '-', '*', '/', '(', ')']:
                self.toks.append({
                    "kind": ch,
                    "value": ch
                })
                index += 1
            else:
                raise Exception("Unkonwn character!")

    def get_token(self):
        """
        获取当前位置的 token
        """
        if 0 <= self.index < len(self.toks):
            tok = self.toks[self.index]
            return tok
        if self.index == len(self.toks):  # token解析结束
            return {
                "kind": "EOF",
                "value": "EOF"
            }
        raise Exception("Encounter Error, invalid index = ", self.index)

    def move_token(self):
        """
        下标向后移动一位
        """
        self.index += 1

    def parse(self) -> Node:
        """
        G -> E
        """
        # 分词
        self.lexizer()
        # 解析
        expr_tree = self.parse_expr()
        if self.parse_end:
            return expr_tree
        else:
            raise Exception("Invalid expression!")

    def parse_expr(self):
        """
        E -> T E'
        E' -> + T E' | - T E' | ɛ
        """
        # E -> E E'
        left = self.parse_term()
        # E' -> + T E' | - T E' | ɛ
        while True:
            tok = self.get_token()
            kind = tok["kind"]
            value = tok["value"]

            if tok["kind"] == "EOF":
                # 解析结束的标志
                self.parse_end = True
                break
            if kind in ["+", "-"]:
                self.move_token()
                left = BinOp(left, value, self.parse_term())
            else:
                break

        return left

    def parse_term(self):
        """
        T -> F T'
        T' -> * F T' | / F T' | ɛ
        """
        # T -> F T'
        left = self.parse_factor()
        # T' -> * F T' | / F T' | ɛ
        while True:
            tok = self.get_token()
            kind = tok["kind"]
            value = tok["value"]

            if kind in ["*", "/"]:
                self.move_token()
                right = self.parse_factor()
                left = BinOp(left, value, right)
            else:
                break

        return left

    def parse_factor(self):
        """
        F -> '(' E ')' | num | name
        """
        tok = self.get_token()
        kind = tok["kind"]
        value = tok["value"]
        if kind == '(':
            self.move_token()
            expr_node = self.parse_expr()
            if self.get_token()["kind"] != ")":
                raise Exception("Encounter Error, expected )!")
            self.move_token()
            return expr_node
        if kind == "INT":
            self.move_token()
            return Constant(value=value)

        raise Exception("Encounter Error, unknown factor: ", kind)


if __name__ == "__main__":
    # 添加命令行参数解析器
    cmd_parser = argparse.ArgumentParser(
        description="Simple Expression Interpreter!")
    group = cmd_parser.add_mutually_exclusive_group()
    group.add_argument("--tokens", help="print tokens", action="store_true")
    group.add_argument("--ast", help="print ast in JSON", action="store_true")
    cmd_parser.add_argument(
        "expr", help="expression, contains ['+', '-', '*', '/', '(', ')', 'num']")
    args = cmd_parser.parse_args()

    calculator = Calculator(expr=args.expr)
    tree = calculator.parse()
    if args.tokens:   # 输出 tokens
        for t in calculator.toks:
            print(f"{t['kind']:3s} ==> {t['value']}")
    elif args.ast:    # 输出 JSON 表示的 AST
        print(json.dumps(tree.visit(), indent=4))
    else:             # 计算结果
        print(tree.eval())

总结

本来想在前面说一下为什么叫 my_eval.py,但是感觉看到后面的人不多,那就在这里说好了。如果写了一个复杂的表达式,那么怎么验证是否正确的。这里我们直接利用 Python 这个最完美的解释器就好了,哈哈。这里用 Python 的 eval 函数,你当然是不需要调用这个函数,直接复制计算的表达式即可。我用 eval 函数,只是想表达为什么我的程序会叫 my_eval 这个名字。

在这里插入图片描述

这样实现下来,也算是完成了一个简单的四则运算解释器了。不过,如果你也做一遍的话,也估计会和我一样感觉到整个过程很繁琐。因为分词和语法解析都有现成的工具可以来完成,而且不容易出错,可以大大减少工作量。不过,自己来一遍也是很有必要的,在使用工具之前,至少也要了解工具的作用。

参考资料

计算机自制解释器Pascal(七):抽象语法树AST
[数据结构]表达式树——手动eval()
来点儿编译原理(1)实现一个小型四则运算编译器
实现一个四则运算语法解析器

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/131672.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

前端框架 Nuxt3 集成axios 配置跨域

目录 一、安装axios 二、Nuxt3项目集成axios 1、项目根目录下创建server/api目录 2、调用封装的单例axios对象 3、页面中调用请求函数 刚开始通过Nuxt3使用axios时&#xff0c;以为axios还需要配置跨域&#xff0c;但经过多次测试发现&#xff0c;在Nuxt3框架里并不需要配…

磨金石教育摄影技能干货分享|胡杨为什么被新疆人奉为精神图腾

痴迷于胡杨的摄影家 新疆摄影师王汉冰&#xff0c;昨天我们介绍了他一张《沙狐之眼.》&#xff0c;天人合一的画面让我们感到震撼。 除此之外王汉冰还有一个称号那就是“胡杨王”。 意思很明确&#xff0c;那就是擅长拍摄胡杨&#xff0c;作品也多是以胡杨见长。 新疆地大物博&…

【OpenCV 例程 300篇】253. 多帧图像(动图)的读取与保存

『youcans 的 OpenCV 例程300篇 - 总目录』 【youcans 的 OpenCV 例程 300篇】253. 多帧图像&#xff08;动图&#xff09;的读取与保存 1. 多帧图像&#xff08;动图&#xff09; 多帧图像是将多幅图像或帧数据保存在单个文件中&#xff0c;也称为多页图像或图像序列&#xf…

eNSP 实现静态路由中的路由备份

eNSP 实现静态路由中的路由备份 eNSP 实现静态路由中的路由备份&#xff1b;路由备份功能&#xff0c;可以提高网络的可靠性。用户可以根据实际情况&#xff0c;配置到同一目的地的多条路由&#xff0c;其中一条路由的优先级最高&#xff0c;作为主路由&#xff0c;其余的路由…

06 retrieveFileStream 之后需要调用 completePendingCommand 否则业务代码会存在问题

前言 问题是这样的 之前 同事碰到了这样的一个问题, 说是基于 ftp 客户端更新文件名字 更新失败 然后 看了一下, 原来是 调用了 retrieveFileStream, 然后 没有同步等待 数据传输完成, 然后 之后直接调用了 rename 的方法 然后 发现 rename 返回的是 false, 并且 文件名…

看我今年奋斗,观我未来之路

看我今年奋斗&#xff0c;观我未来之路 。 博客之星评选已经进行了几天。我也没有很去关注这东西&#xff0c;毕竟一个刚注册一年、写了无数水文的误导他人的博主&#xff0c;怎么可能拿到博客之星&#xff1f; 我&#xff0c;只是无聊时转发一下&#xff0c;那句毫无新意、从…

大学生如何在网上赚零花钱,适合学生党可做的零花钱项目

大学生的课余时间是非常多的&#xff0c;利用这些时间&#xff0c;我们可以去做点小兼职赚点零花钱&#xff0c;既可以补贴生活费&#xff0c;又可以获得不一样的生活体验。 现在大学生完全可以不用去大街上发传单&#xff0c;或者去咖啡店当服务员了&#xff0c;自己在网上就…

【数据结构】单链表(线性表)的实现

目录 一、什么是链表 二、单链表的实现 1、动态申请一个结点 2、单链表打印 3、单链表尾插 4、单链表的尾删 5、单链表的头插 6、单链表头删 7、单链表查找 8、单链表在pos位置之后插入x 9、单链表删除pos位置之后的值 10、单链表在pos位置之前插入x 11、单链表删除pos位置的值…

前端期末考试试题及参考答案(04)

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl 一、 填空题 在页面中&#xff0c; ______标签用于创建一个表单。< form>中的______属性用于指定接收并处理表单数据的服务器url地址。< form>中的______表示以…

基于springboot+Vue的疫情防控系统(程序+数据库)

大家好✌&#xff01;我是CZ淡陌。一名专注以理论为基础实战为主的技术博主&#xff0c;将再这里为大家分享优质的实战项目&#xff0c;本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路…

Linux 资源限制 setrlimit

有的时候为了避免程序毫无意义的占用CPU&#xff08;如死循环&#xff09;、过度占用内存&#xff08;如内存泄漏&#xff09;&#xff0c;我们可以限制程序使用的资源。 下面主要从两个角度限制资源&#xff1a; 限制程序累计运行时长限制可以使用的内存大小 限制资源使用到…

微信小程序 | 小程序系统API调用

&#x1f5a5;️ 微信小程序专栏&#xff1a;小程序系统API调用 &#x1f9d1;‍&#x1f4bc; 个人简介&#xff1a;一个不甘平庸的平凡人&#x1f36c; ✨ 个人主页&#xff1a;CoderHing的个人主页 &#x1f340; 格言: ☀️ 路漫漫其修远兮,吾将上下而求索☀️ &#x1f44…

Go语言进阶

一、并发 VS 并行 1: 多线程程序在一个核的CPU上运行 2: 多线程程序在多个核的CPU上运行 Go可以充分发挥多核优势&#xff0c;高效运行。 线程&#xff1a;用户态&#xff0c;轻量级线程&#xff0c;栈MB级别。 协程&#xff1a;内核态&#xff0c;线程内跑多个协程&#xff…

最短路径的java代码实现

1.最短路径定义及性质 有了加权有向图之后&#xff0c;我们立刻就能联想到实际生活中的使用场景&#xff0c;例如在一副地图中&#xff0c;找到顶点a与地点b之间的路径&#xff0c;这条路径可以是距离最短&#xff0c;也可以是时间最短&#xff0c;也可以是费用最小等&#xff…

爬虫的奇技淫巧之ajax-hook

声明 本文仅供学习参考&#xff0c;如有侵权可私信本人删除&#xff0c;请勿用于其他途径&#xff0c;违者后果自负&#xff01; 前言 随着反爬力度的不断升级&#xff0c;现在的爬虫越来越难搞了。诸如加密参数sign、signature、token。面对这种情况传统的方式可以使用自动…

Redis集群系列九 —— 集群伸缩之扩容

集群伸缩 Redis 集群提供了灵活的节点扩容和收缩方案&#xff0c;当有新节点加入时&#xff0c;需要把一部分数据迁移到新节点来达到集群的负载均衡&#xff1b;当旧节点退出时&#xff0c;需要把其上的数据迁移到其他节点上&#xff0c;确保该节点上的数据能够被正常访问。从…

ext4 extent详解3之内核源码流程讲解

本文在前两篇《ext4 extent详解1之示意图演示》和《ext4 extent详解2之内核源码详解》讲解ext4 extent 文章的基础上&#xff0c;本文从内核源码、实例演示等角度详细介绍ext4 extent B树的形成过程&#xff0c;希望看过本文的读者能理解ext4 extent B树的工作原理。 1 &#…

6.3 返回类型和return语句

文章目录无返回值函数有返回值函数值是如何被返回的不要返回局部对象的引用或指针引用返回左值列表初始化返回值主函数main的返回值递归返回数组指针声明一个返回数组指针的函数使用尾置返回类型使用decltypereturn语句终止当前正在执行的函数并将控制权返回到调用该函数的地方…

2022年终总结:点滴积累让我不再迷茫

今年是开始写作的第二年&#xff0c;如果说第一年是起步的话&#xff0c;今年就是开始有了一些小收获了&#xff0c;通过点滴积累让我知道积累的充实感&#xff0c;通过一点一点粉丝或阅读量的积累&#xff0c;增加写作的自信。 今年的收获 首先看一下今年的阅读量和粉丝量: …

cheunghonghui的【22年度总结】

cheunghonghui的【22年度总结】 好久好久没写博客了&#xff0c;看了下后台&#xff0c;上一次发表博客已经是一年半之前&#xff0c;趁着年底&#xff0c;抓紧时间写&#xff08;水&#xff09;一篇不然就要断更了。 【年度工作总结】 1、迭代了未知bug 2、修复了已知bug …