当前位置: 首页 > news >正文

1111111111


 

实现计算器之前,先给出实现这个计算器的大致地图:

我们会支持下面的表达式:

345*(3+5)


 

我们先大致讲下实现这个计算器的流程。首先,我们要实现一个扫描器,把我们输入的代码:345*(3+5)转换成token数据。

我们要从345*(3+5)中对应的词素(lexemes)中生成标记(token),在这里,345三个字符代表一个数字三百四十五,这三个字符连在一起是有意义的,但如果是单个的3或4或5,是没意义的。所以,扫描器 的目的就是把对应的字符串,处理成一个一个有意义的词素对应的token数据。所以上面的345要连起来处理成一个词素的token,而后面的单个字符*就可以处理成一个词素的token,因为*单个字符就有意义,它代表的是算术法则里的乘以。上面表达式有下面几个词素:

345,*,(,3,+,5,)共七个词素。

token数据有好几个维度,比如这个token的类别,它在原来字符串中的text, 它代码的值(字面量)是多少。生成的token是为了下面的解析器(parser)服务的,会作为parser的输入,parser根据token记录的类别,来分别对应处理。比如如果当前token的类别是"NUMBER",就会生成Literal类型的表达式,而如果是"+"号,就会递归下降调用乘法的方法。又比如:我们知道,每一门语言都有自己的关键词(keywords),比如python里有for ,while, def, class,我们的解析器就可以根据这些不同的关键词来分析处理,像for,我们会把它生成的token的类别设置成FOR。这样解析器就可以根据token的类别来对应处理逻辑。我们现在实现的计算器没有关键词后面在实现parser的时候,再回头来看这段,就会明白了。讲这一点是想表达,扫描器,解析器,解释器它们是分层处理,每一层都有输出的数据类型,以供下一层作为输入使用,到实现解释器(interpreter)的时候,同样会看到,第二层解析器(parser)生成的不同表达式,如Binary,Grouping,Unary,也会作为下一层解释器(interpreter)的输入,然后解释器同样根据这些表达式的不同属性,分别对应处理相应逻辑。刚才说了,现在不明白这些没关系。但头脑中要有个分层的思想。



 

扫描器生成的token数据可以用数据结构封装,如果不考虑其它方面,用对象,元组,字典都是可以的。我们先假装自己是个初学者,先用元组试着封装下。所以,345*(3+5)就可以封装成以下几个token:

我们规定:元组第一个值代表token的类型,第二个值代码token在源字符中的文本,第三个值代码源字符中符号代表的字面量(专业说法叫运行时对象。比如345这个字符串,如果用python表示,应该表示成345.0这样的浮点类型,在我们计算器中,只有数字才有代表的字面量值,其它的字面量值都是None)

345 -> ("NUMBER", "345", 345.0), 说明:345三个字符组成一个数字类型(所以第一个值是"NUMBER")的token, 它在原字符串是中“345”的字符串文本,它代表值是345.0,用python表示的浮点值的类型。

* -> ("STAR", "*", None), 说明:*代表乘法,我们用"STAR"表示类型, 在源代码中是"*"字符,它是一个操作符合,不是数字,所以它的值我们用python表示,就是None,如果我们是用java实现的,那就用null,

( -> ("LEFT_PAREN", "(", None ), 说明:(语言中的括号,如果我们在表达式中用了(),这里面的表达式我们是要优先计算的。我们用"LEFT_PAREN"表示类型, 在源代码中是"("字符,它是一个操作符合,不是数字,所以它的值我们用python表示,就是None

)-> ("LEFT_PAREN", "(", None ), 说明同上面的(。


 

我们先实现这个计算器的扫描器。你可以先把代码下载到本地,跑起来先感性直观的体会下。

好,我们现在开始讲解下代码实现。

python3 tl.py运行文件,可以打断点一步步跟着看代码。


 

def main():

    """

    这个方法主要实现:根据参数的长度决定是用文件还是从命令行输入

    """

    args = sys.argv[1:]

    if len(args)>1:

        print("usage:python ")

    elif len(args)==1:

        runFile()

    else:

        runPrompt()

这是入口文件tl.py的主方法,这里支持根据参数来决定是从命令行还是文件中读取字符,我们先实现简单的计算器,所以进入下面的runPrompt()方法:

def runPrompt():

    while True:

        try:

            line = input("> ")

            if line == "":

                break

            run(line)

            had_error = False

        except EOFError:

            break

        except Exception as e:

            print(f"An error occurred: {e}")

这个函数会调用run方法:

def run(line):

    tokens = scanTokens(line)

    print(tokens)

这样分开写是为了解耦,因为后面的实现runFile()方法也会调用run()方法。

现在真正进入我们的扫描器了,进入scanTokens()方法:

def scanTokens(source):

    """

    如果是只有+-*/和数字。输入是:2+4*3,变成下面和形式:

    NUMBER 2 2.0

    PLUS + null

    NUMBER 4 4.0

    STAR * null

    NUMBER 3 3.0

    """

    tokens = []

    start = 0

    current = 0

    while not isAtEnd(source, current):

        start = current

        c = source[current]

        if c == '+':

            tokens.append(("PLUS", c, None))

            current += 1

        elif c == '-':

            tokens.append(("MINUS", c, None))

            current += 1

        elif c == '*':

            tokens.append(("STAR", c, None))

            current += 1

        elif c == '/':

            tokens.append(("SLASH", c, None))

            current += 1

        elif c == "(":

            tokens.append(("LEFT_PAREN", c, None))

            current += 1

        elif c == ")":

            tokens.append(("LEFT_PAREN", c, None))

            current += 1

        else:

            while isDigit(source[current]):

                current += 1

                if isAtEnd(source, current):

                    break

            text = source[start: current]

            obj = float(text)

            tokens.append(("NUMBER", text, obj))

    return tokens

这段代码实现逻辑很简单,就是循环遍历字符串,然后生成tokens列表并且返回

tokens = []

用来存储最后生成的token

start = 0

current = 0

start是记录每个词素开始的位置,所以,每次生成完一个token,start的值都会更新:

start = current

这段代码有几个要说的点:

def isAtEnd(source, current):

    return current >= len(source)


 

def isDigit(c):

    return c >= '0' and c <= '9'

我定义了两个辅助函数,一个判断循环是否到最后了,一个判断当前字符是否是数字。

因为我们实现的是计算器,所以这个循环里只有两种情况,一种是单个字符,比如+,-,*,/,(,)。一种是数字,这种就复杂点:代码如下:

while isDigit(source[current]):

                current += 1

                if isAtEnd(source, current):

                    break

            text = source[start: current]

            obj = float(text)

            tokens.append(("NUMBER", text, obj))

    return tokens

当前字符如果是数字,会不断循环看下一个字符,如果也是数字,就一直循环,直到不是数字。标记字符位置的current随着不断加1,而start还是一开头的值。这时我们就通过start和current的区间记录出来当前数字的文本字符。比如上面例子的345。我们后面的解释器是使用python的float类型来表示数字,所以把这个文本字符通过python的float()把它转换成345.0,这个值后面会被解释器(interpreter)使用。最后,把上面两步的值封装到一个元组中:

text = source[start: current]

obj = float(text)

tokens.append(("NUMBER", text, obj))

说明:为了最小实现计算器,我们的计算器不直持小数运算。我们一开始的目的是为了体会实现一门语言的大致框架,先把一些细节隐藏起来。后面随着迭代会支持的。

我是用面向过程实现这个简单的扫描器,读者后面可以把它改装成面向对象。体会下面向对象的好处。比如把上面记录字符位置的start,current,以及存入token的tokens作为类的属性。

到此,我们的扫描器就完成了。读者可以自己在这个扫描器的基础上玩一玩,比如刚才说的:如何支持小数的运算?

下一讲,我们接着实现一个解析器(parser),上面提到过,解析器是用我们这一讲生成的token列表,来组装成更丰富,更复杂的表示形式----树。会很有意思,也更有挑战。


 

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 微服务:网关路由和登录校验
  • 计算机视觉与视觉大模型对板书检测效果对比
  • 上线eleme项目
  • 怎么整合spring security和JWT
  • 【Unity3D小技巧】Unity3D中实现FPS数值显示功能实现
  • CSS 的了解text-rendering属性
  • 大模型学习笔记 - LLM 之 LLaMA系列(待更新)
  • 缺失ffmpeg.dll要用什么修复方法?快速恢复丢失的ffmpeg.dll文件
  • C++基础面试题 | C和C++的区别?
  • 【小趴菜前端学习日记3】
  • 【速览】计算机网络(更新中)
  • 使用VRoid Studio二次元建模,创建专属于自己的二次元卡通人物模型,创建完全免费开源且属于自己VRM模型
  • css 宫格样式内容上下结构
  • 井盖异动传感器:为城市安全加码
  • 电机启动对单片机重启的影响
  • Android开发 - 掌握ConstraintLayout(四)创建基本约束
  • CentOS从零开始部署Nodejs项目
  • echarts的各种常用效果展示
  • ES学习笔记(10)--ES6中的函数和数组补漏
  • flask接收请求并推入栈
  • Java知识点总结(JavaIO-打印流)
  • React的组件模式
  • select2 取值 遍历 设置默认值
  • Unix命令
  • 初探 Vue 生命周期和钩子函数
  • 短视频宝贝=慢?阿里巴巴工程师这样秒开短视频
  • 简析gRPC client 连接管理
  • 那些年我们用过的显示性能指标
  • 算法-图和图算法
  • 再次简单明了总结flex布局,一看就懂...
  • MPAndroidChart 教程:Y轴 YAxis
  • 测评:对于写作的人来说,Markdown是你最好的朋友 ...
  • 专访Pony.ai 楼天城:自动驾驶已经走过了“从0到1”,“规模”是行业的分水岭| 自动驾驶这十年 ...
  • ​【C语言】长篇详解,字符系列篇3-----strstr,strtok,strerror字符串函数的使用【图文详解​】
  • ​secrets --- 生成管理密码的安全随机数​
  • ​你们这样子,耽误我的工作进度怎么办?
  • $$$$GB2312-80区位编码表$$$$
  • (8)Linux使用C语言读取proc/stat等cpu使用数据
  • (JS基础)String 类型
  • (Redis使用系列) Springboot 实现Redis消息的订阅与分布 四
  • (二十五)admin-boot项目之集成消息队列Rabbitmq
  • (附源码)ssm跨平台教学系统 毕业设计 280843
  • (十三)Flask之特殊装饰器详解
  • (一)SvelteKit教程:hello world
  • .Net CoreRabbitMQ消息存储可靠机制
  • .net framwork4.6操作MySQL报错Character set ‘utf8mb3‘ is not supported 解决方法
  • .net 发送邮件
  • .Net 转战 Android 4.4 日常笔记(4)--按钮事件和国际化
  • [1159]adb判断手机屏幕状态并点亮屏幕
  • [Android]使用Git将项目提交到GitHub
  • [Angular] 笔记 20:NgContent
  • [AutoSar]BSW_Com02 PDU详解
  • [C#]C# OpenVINO部署yolov8图像分类模型
  • [C#]winform基于深度学习算法MVANet部署高精度二分类图像分割onnx模型高精度图像二值化
  • [CF482B]Interesting Array