用Python编写个解释器实现方法接受

目录
  • 前言
  • 标记(Token)
  • 词法分析器(Lexer)
  • 解析器(Parser)
  • 结论

前言

在本文中,我们将设计一个可以执行算术运算的解释器。

我们不会重新造轮子。文章将使用由 David M. Beazley 开发的词法解析器 —— PLY(Python Lex-Yacc(https://github.com/dabeaz/ply))。

PLY 可以通过以下方式下载:

$ pip install ply

我们将粗略地浏览一下创建解释器所需的基础知识。欲了解更多,请参阅这个 GitHub 仓库(https://github.com/dabeaz/ply)。

标记(Token)

标记是为解释器提供有意义信息的最小字符单位。标记包含一对名称和属性值。

让我们从创建标记名称列表开始。这是一个必要的步骤。

tokens = (
    # 数据类型
    "NUM",
    "FLOAT",
    # 算术运算
    "PLUS",
    "MINUS",
    "MUL",
    "DIV",
    # 括号
    "LPAREN",
    "RPAREN",
)

词法分析器(Lexer)

将语句转换为标记的过程称为标记化或词法分析。执行词法分析的程序是词法分析器。

# 标记的正则表达
t_PLUS   = r"\+"
t_MINUS  = r"\-"
t_MUL    = r"\*"
t_DIV    = r"/"
t_LPAREN = r"\("
t_RPAREN = r"\)"
t_POW    = r"\^"
# 忽略空格和制表符
t_ignore = " \t"
# 为每个规则添加动作
def t_FLOAT(t):
    r"""\d+\.\d+"""
    t.value = float(t.value)
    return t
def t_NUM(t):
    r"""\d+"""
    t.value = int(t.value)
    return t
# 未定义规则字符的错误处理
def t_error(t):
    # 此处的 t.value 包含未标记的其余输入
    print(f"keyword not found: {t.value[0]}\nline {t.lineno}")
    t.lexer.skip(1)
# 如果遇到 \n 则将其设为新的一行
def t_newline(t):
    r"""\n+"""
    t.lexer.lineno += t.value.count("\n")

为导入词法分析器,我们将使用:

importply.lexaslex

t_ 是一个特殊的前缀,表示定义标记的规则。每条词法规则都是用正则表达式制作的,与 Python 中的 re 模块兼容。正则表达式能够根据规则扫描输入并搜索符合的符号串。正则表达式定义的文法称为正则文法。正则文法定义的语言则称为正则语言。

定义好了规则,我们将构建词法分析器。

data = 'a = 2 +(10 -8)/1.0'
lexer = lex.lex()
lexer.input(data)
while tok := lexer.token():
    print(tok)

为了传递输入字符串,我们使用 lexer.input(data)。lexer.token() 将返回下一个 LexToken 实例,最后返回 None。根据上述规则,代码 2 + ( 10 -8)/1.0 的标记将是:

紫色字符代表的是标记的名称,其后是标记的具体内容。

巴科斯-诺尔范式(Backus-Naur Form,BNF)

大多数编程语言都可以用上下文无关文法来编写。它比常规语言更复杂。对于上下文无关文法,我们用上下文无关语法,它是描述语言中所有可能语法的规则集。BNF 是一种定义语法的方式,它描述了编程语言的语法。让我们看看例子:

symbol : alternative1 | alternative2 …

根据产生式,: 的左侧被替换为右侧的其中一个值替换。右侧的值由 | 分隔(可理解为 symbol 定义为 alternative1 或 alternative2或…… 等等)。对于我们的这个算术解释器,语法规格如下:

expression : expression '+' expression
           | expression '-' expression
           | expression '/' expression
           | expression '*' expression
           | expression '^' expression
           | +expression
           | -expression
           | ( expression )
           | NUM
           | FLOAT

输入的标记是诸如 NUM、FLOAT、+、-、*、/ 之类的符号,称作终端(无法继续分解或产生其他符号的字符)。一个表达式由终端和规则集组成,例如 expression 则称为非终端。

解析器(Parser)

我们将使用 YACC(Yet Another Compiler Compiler) 作为解析器生成器。导入模块:import ply.yacc as yacc。

from operator import (add, sub, mul, truediv, pow)
# 我们的解释器支持的运算符列表
ops = {
    "+": add,
    "-": sub,
    "*": mul,
    "/": truediv,
    "^": pow,
}
def p_expression(p):
    """expression : expression PLUS expression
                  | expression MINUS expression
                  | expression DIV expression
                  | expression MUL expression
                  | expression POW expression"""
    if (p[2], p[3]) == ("/", 0):
        # 如果除以 0,则将“INF”(无限)作为值
        p[0] = float("INF")
    else:
        p[0] = ops[p[2]](p[1], p[3])
def p_expression_uplus_or_expr(p):
    """expression : PLUS expression %prec UPLUS
                  | LPAREN expression RPAREN"""
    p[0] = p[2]
def p_expression_uminus(p):
    """expression : MINUS expression %prec UMINUS"""
    p[0] = -p[2]
def p_expression_num(p):
    """expression : NUM
                  | FLOAT"""
    p[0] = p[1]
# 语法错误时的规则
def p_error(p):
    print(f"Syntax error in {p.value}")

在文档字符串中,我们将添加适当的语法规范。p 列表中的的元素与语法符号一一对应,如下所示:

expression : expression PLUS expression
p[0]         p[1]       p[2] p[3]

在上文中,%prec UPLUS 和 %prec UMINUS 是用来表示自定义运算的。%prec 即是 precedence 的缩写。在符号中本来没有 UPLUS 和 UMINUS 这个说法(在本文中这两个自定义运算表示一元正号和符号,其实 UPLUS 和 UMINUS 只是个名字,想取什么就取什么)。之后,我们可以添加基于表达式的规则。YACC 允许为每个令牌分配优先级。我们可以使用以下方法设置它:

precedence = (
    ("left", "PLUS", "MINUS"),
    ("left", "MUL", "DIV"),
    ("left", "POW"),
    ("right", "UPLUS", "UMINUS")
)

在优先级声明中,标记按优先级从低到高的顺序排列。PLUS 和 MINUS 优先级相同并且具有左结合性(运算从左至右执行)。MUL 和 DIV 的优先级高于 PLUS 和 MINUS,也具有左结合性。POW 亦是如此,不过优先级更高。UPLUS 和 UMINUS 则是具有右结合性(运算从右至左执行)。

要解析输入我们将使用:

parser = yacc.yacc()
result = parser.parse(data)
print(result)

完整代码如下:

#####################################
# 引入模块                           #
#####################################
from logging import (basicConfig, INFO, getLogger)
from operator import (add, sub, mul, truediv, pow)
import ply.lex as lex
import ply.yacc as yacc
# 我们的解释器支持的运算符列表
ops = {
    "+": add,
    "-": sub,
    "*": mul,
    "/": truediv,
    "^": pow,
}
#####################################
# 标记集                             #
#####################################
tokens = (
    # 数据类型
    "NUM",
    "FLOAT",
    # 算术运算
    "PLUS",
    "MINUS",
    "MUL",
    "DIV",
    "POW",
    # 括号
    "LPAREN",
    "RPAREN",
)
#####################################
# 标记的正则表达式                    #
#####################################
t_PLUS   = r"\+"
t_MINUS  = r"\-"
t_MUL    = r"\*"
t_DIV    = r"/"
t_LPAREN = r"\("
t_RPAREN = r"\)"
t_POW    = r"\^"
# 忽略空格和制表符
t_ignore = " \t"
# 为每个规则添加动作
def t_FLOAT(t):
    r"""\d+\.\d+"""
    t.value = float(t.value)
    return t
def t_NUM(t):
    r"""\d+"""
    t.value = int(t.value)
    return t
# 未定义规则字符的错误处理
def t_error(t):
    # 此处的 t.value 包含未标记的其余输入
    print(f"keyword not found: {t.value[0]}\nline {t.lineno}")
    t.lexer.skip(1)
# 如果看到 \n 则将其设为新的一行
def t_newline(t):
    r"""\n+"""
    t.lexer.lineno += t.value.count("\n")
#####################################
# 设置符号优先级                      #
#####################################
precedence = (
    ("left", "PLUS", "MINUS"),
    ("left", "MUL", "DIV"),
    ("left", "POW"),
    ("right", "UPLUS", "UMINUS")
)
#####################################
# 书写 BNF 规则                      #
#####################################
def p_expression(p):
    """expression : expression PLUS expression
                  | expression MINUS expression
                  | expression DIV expression
                  | expression MUL expression
                  | expression POW expression"""
    if (p[2], p[3]) == ("/", 0):
        # 如果除以 0,则将“INF”(无限)作为值
        p[0] = float("INF")
    else:
        p[0] = ops[p[2]](p[1], p[3])
def p_expression_uplus_or_expr(p):
    """expression : PLUS expression %prec UPLUS
                  | LPAREN expression RPAREN"""
    p[0] = p[2]
def p_expression_uminus(p):
    """expression : MINUS expression %prec UMINUS"""
    p[0] = -p[2]
def p_expression_num(p):
    """expression : NUM
                  | FLOAT"""
    p[0] = p[1]
# 语法错误时的规则
def p_error(p):
    print(f"Syntax error in {p.value}")
#####################################
# 主程式                             #
#####################################
if __name__ == "__main__":
    basicConfig(level=INFO, filename="logs.txt")
    lexer = lex.lex()
    parser = yacc.yacc()
    while True:
        try:
            result = parser.parse(
                input(">>>"),
                debug=getLogger())
            print(result)
        except AttributeError:
            print("invalid syntax")

结论

由于这个话题的体积庞大,这篇文章并不能将事物完全的解释清楚,但我希望你能很好地理解文中涵盖的表层知识。

到此这篇关于用Python编写个解释器实现方法接受的文章就介绍到这了,更多相关Python解释器内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 利用 Python 开发一个 Python 解释器

    目录 1.标记(Token) 2.词法分析器(Lexer) 3.巴科斯-诺尔范式(Backus-Naur Form,BNF) 4.解析器(Parser) 前言: 计算机只能理解机器码.归根结底,编程语言只是一串文字,目的是为了让人类更容易编写他们想让计算机做的事情.真正的魔法是由编译器和解释器完成,它们弥合了两者之间的差距.解释器逐行读取代码并将其转换为机器码. 在本文中,我们将设计一个可以执行算术运算的解释器. 我们不会重新造轮子.文章将使用由 David M. Beazley 开发的词法解析

  • Python3 解释器的实现

    Linux/Unix的系统上,一般默认的 python 版本为 2.x,我们可以将 python3.x 安装在 /usr/local/python3 目录中. 安装完成后,我们可以将路径 /usr/local/python3/bin 添加到您的 Linux/Unix 操作系统的环境变量中,这样您就可以通过 shell 终端输入下面的命令来启动 Python3 . $ PATH=$PATH:/usr/local/python3/bin/python3 # 设置环境变量 $ python3 --ve

  • Python 设计模式行为型解释器模式

    目录 一.解释器模式 二.应用场景 三.代码示例 一.解释器模式 解释器模式,开发者自定义一种 “有内涵” 的语言(或者叫字符串),并设定相关的解释规则,输入该字符串后可以输出公认的解释,或者执行程序可以理解的动作. 优点: 可扩展性比较好,灵活. 增加了新的解释表达式的方式. 易于实现简单文法. 缺点: 可利用场景比较少. 对于复杂的文法比较难维护. 解释器模式会引起类膨胀. 二.应用场景 SQL 解析 符号处理引擎 三.代码示例 实体角色: 终结符表达式:实现与文法中的元素相关联的解释操作,

  • Python新手入门之解释器的安装

    一.Python简介 1.python介绍 Python由荷兰数学和计算机科学研究学会的Guido van Rossum 于1990 年代初设计,作为一门叫做ABC语言的替代品. [1] Python提供了高效的高级数据结构,还能简单有效地面向对象编程.Python语法和动态类型,以及解释型语言的本质,使它成为多数平台上写脚本和快速开发应用的编程语言, [2] 随着版本的不断更新和语言新功能的添加,逐渐被用于独立的.大型项目的开发. 2.python特点 ①Python优点: (1)简单易学 (

  • 详解python学习笔记之解释器

    目录 1.python教程 1.1 概述 1.2 python标准库 1.3 python语言参考手册 1.4 python包索引 1.5 术语对照表 2.课前甜点 3.python解析器 3.1 传入参数 3.2 交互式运行 3.2.1 可执行的Python脚本 3.2.2 交互式启动文件 3.3 解释器的运行环境( 源文件的字符编码) 总结 1.python教程 基于 python3.10 的持续解读,旨在快速回忆加深理解,节约自己的时间成本 1.1 概述 python 是一门易于学习的编程

  • 超详细Python解释器新手安装教程

    Step1:确定操作系统 Python 解释器的下载地址为:https://www.python.org/ ,点击 "Downloads"选项如下图所示: 可以看到最新版为 Python3.8.2,接下来根据自己的情况选择相应的电脑系统,如点击"Windows"选项进入详细的下载列表: 上图中可以看到一共有 7 个下载链接,第 1 个为帮助文档,其余 6 个根据操作系统位数不同分为 3 类,以 64 位操作系统为例 3 类安装包描述如下图: Step2:下载离线安装

  • 用Python编写个解释器实现方法接受

    目录 前言 标记(Token) 词法分析器(Lexer) 解析器(Parser) 结论 前言 在本文中,我们将设计一个可以执行算术运算的解释器. 我们不会重新造轮子.文章将使用由 David M. Beazley 开发的词法解析器 —— PLY(Python Lex-Yacc(https://github.com/dabeaz/ply)). PLY 可以通过以下方式下载: $ pip install ply 我们将粗略地浏览一下创建解释器所需的基础知识.欲了解更多,请参阅这个 GitHub 仓库

  • Python编写条件分支代码方法

    目录 前言 最佳实践 1.避免多层分支嵌套 2. 封装那些过于复杂的逻辑判断 3. 留意不同分支下的重复代码 4. 谨慎使用三元表达式 常见技巧 1.使用“德摩根定律” 2. 自定义对象的“布尔真假” 3. 在条件判断中使用 all() / any() 4. 使用 try/while/for 中 else 分支 常见 1.与 None 值的比较 2. 留意 and 和 or 的运算优先级 结语 前言 编写条件分支代码是编码过程中不可或缺的一部分.如果用道路来做比喻,现实世界中的代码从来都不是一条

  • Atom Python 配置Python3 解释器的方法

    环境 Mac Python3.6.4 Atom 背景 Atom 执行Python Code 使用Script Package,执行快捷键cmd + i. 但是默认是执行Mac 系统的2.7 版本的Python. 配置 cmd + ,(cmd + 逗号) 快捷键 打开Settings,或者点击Atom→Preferences 打开Settings 点击Open Config Folder(会打开Atom 的Project) 打开 .atom/packages/script/lib/grammars

  • Python编写登陆接口的方法

    本文实例为大家分享了Python编写登陆接口的具体代码,供大家参考,具体内容如下 1.输入用户名密码: 2.认证成功后显示欢迎信息: 3.错误三次后,账号被锁定. 账号文件:user.txt 锁定文件:locked.txt 流程图如下: # -*- coding:utf-8 -*- # Author Caoxl import sys account_file='E:\user.txt' locked_file='E:\locked.txt' def deny_account(username):

  • 使用Python编写Prometheus监控的方法

    要使用python编写Prometheus监控,需要你先开启Prometheus集群.可以参考//www.jb51.net/article/148895.htm 安装.在python中实现服务器端.在Prometheus中配置请求网址,Prometheus会定期向该网址发起申请获取你想要返回的数据. 使用Python和Flask编写Prometheus监控 Installation pip install flask pip install prometheus_client Metrics P

  • python实现简单登陆流程的方法

    登陆流程图: 代码实现: #-*- coding=utf-8 -*- import os,sys,getpass ''' user.txt 格式 账号 密码 是否锁定 错误次数 jack 123 unlock 0 tom 123 unlock 0 lily 123 unlock 0 hanmeimei 123 unlock 0 lucy 123 unlock 0 ''' # 定义写入文件的函数 def wirte_to_user_file(users,user_file_path): user_

  • 用Python编写一个简单的Lisp解释器的教程

    本文有两个目的: 一是讲述实现计算机语言解释器的通用方法,另外一点,着重展示如何使用Python来实现Lisp方言Scheme的一个子集.我将我的解释器称之为Lispy (lis.py).几年前,我介绍过如何使用Java编写一个Scheme解释器,同时我还使用Common Lisp语言编写过一个版本.这一次,我的目的是尽可能简单明了地演示一下Alan Kay所说的"软件的麦克斯韦方程组" (Maxwell's Equations of Software). Lispy支持的Scheme

  • 使用python编写udp协议的ping程序方法

    服务器端 import random from socket import * serverSocket = socket(AF_INET, SOCK_DGRAM)#建立udp协议的socket连接 serverSocket.bind(('', 12000)) while True: rand = random.randint(0, 10)#生成随机数,模拟udp环境下的丢包 message, address = serverSocket.recvfrom(1024)#接收客户端发送的信息,应该

  • 在Python文件中指定Python解释器的方法

    以下针对Ubuntu系统,Windows系统没有测试过. Ubuntu中默认就安装有Python 2.x和Python 3.x,默认情况下python命令指的是Python 2.x.因此当将Python脚本设为可执行文件直接在命令行里执行时,系统调用的是Python 2.x的解释器. 如果在直接执行Python脚本(例如在命令行直接输入xxx.py)时,想调用Python 3.x解释器去解释脚本,一种方法是修改符号链接,让python命令指向Python3.这种方法在自己的系统上还行得通,如果脚

  • Python编写带选项的命令行程序方法

    运行python程序时,有时需要在命令行传入一些参数.常见的方式是在执行时,在脚本名后直接追加空格分隔的参数列表(例如 python test.py arg0 arg1 arg2),然后在脚本中就可以通过sys.argv获取所有的命令行参数. 这种方式的优点是传参方便,参数获取简单:缺点是执行脚本时,必须知道参数的顺序,并且不能设置默认值,所有参数每次都必须传入. 还有一种命令行传参方式是通过带选项的方式进行传参(例如python test.py -p0=arg0 -p1=arg1). 这种方式

随机推荐