用 Python 编写的 Python 解释器,你会吗?

news2024/11/27 12:36:26

计算机只能理解机器码。归根结底,编程语言只是一串文字,目的是为了让人类更容易编写他们想让计算机做的事情。真正的魔法是由编译器和解释器完成,它们弥合了两者之间的差距。解释器逐行读取代码并将其转换为机器码。

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

我们不会重新造轮子。文章将使用由 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")

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

import ply.lex as lex

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")

结论

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

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

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

相关文章

java计算机毕业设计基于安卓Android的儿童疫苗接种管理App

项目介绍 本文讲述了儿童疫苗管理App的设计与实现。结合电子管理系统的特点,分析了儿童疫苗管理App的现状,给出了儿童疫苗管理App实现的设计方案。 本论文主要完成不同用户的权限划分,不同用户具有不同权限的操作功能,在客户端,主要有管理员、医生和用户三个角色可以登录,用户…

【react】JSX基本语法

1、全称 JavaScript XML 2、定义 是react定义的一种类似于XML的JS扩展语法: JS XML本质是React.createElement(component, props, …children)方法的语法糖 3、作用: 用来简化创建虚拟DOM 4、标签名任意 HTML标签或其它标签 5、标签属性任意 HTML标签属性或其它 6、基本语法规…

Python 缩进语法的起源:上世纪 60-70 年代的大胆创意!

上个月,Python 之父 Guido van Rossum 在推特上转发了一篇文章《The Origins of Python》,引起了我的强烈兴趣。 众所周知,Guido 在 1989 年圣诞节期间开始创造 Python,当时他就职于荷兰数学和计算机科学研究学会(简称…

SAP ABAP CDS view 里 INNER JOIN 和 Association 的区别

最近有朋友在我的知识星球里向我提问,SAP ABAP CDS view 的 INNER JOIN 和 Association 的功能可以理解为一样吗? (关于加入我的知识星球的方式,请移步本文文末) 本文就来聊一聊这个话题。既然 CDS view 里同时支持了 INNER JOIN 和 Assoc…

小伙伴因 unshift 插入数据被批,未曾想到找我诉苦竟梅开二度

背景 事情是这样的,今天小伙伴跟我诉苦,说写的代码被批了,原因是效率太低了,简单问了一下需求,就是将几千条数据倒序插入到数组中,他是通过循环搭配 unshift 实现的,听完我也批了他一顿。 小伙…

ImageNet

标题有点不太对是的 就能用了 这次是说 用有噪声的学生网络进行自我训练提高ImageNet分类 近年来,深度学习在图像识别方面取得了显著的成功。然而,最先进的视觉模型仍然是用监督学习来训练的,这就需要大量的标记图像才能很好地工作。 通过只…

监控易火星版纳管IPv6:IP地址管理V2.0的进化之路

IPv6的使用,不仅能解决网络地址资源数量的问题,而且也解决了多种接入设备连入互联网的障碍。大量IP地址的接入,导致运维人员不得不投入大量精力来解决IP管理分散、非法接入、IP地址错误、IP地址冲突等导致的问题。随之而来的,IP地…

java计算机毕业设计基于安卓Android的校园助手APP

项目介绍 网络的广泛应用给生活带来了十分的便利。所以把校园助手与现在网络相结合,利用java技术建设校园助手APP,实现校园助手的信息化。则对于进一步提高校园助手发展,丰富校园助手经验能起到不少的促进作用。 校园助手APP能够通过互联网得到广泛的、全面的宣传,让尽可能多的…

Metal每日分享,图像单色滤镜效果

本案例的目的是理解如何用Metal实现图像单色效果滤镜,将图像转换为单色版本,根据每个像素的亮度进行着色; Demo HarbethDemo地址 实操代码 // 去雾效果滤镜 let filter C7Monochrome.init(intensity: 0.83, color: .blue)// 方案1: Image…

Zookeeper[1]-Zookeeper介绍与安装以及集群环境准备

Java 是第一大编程语言和开发平台。它有助于企业降低成本、缩短开发周期、推动创新以及改善应用服务。如今全球有数百万开发人员运行着超过 51 亿个 Java 虚拟机,Java 仍是企业和开发人员的首选开发平台 课程内容的介绍 Zookeeper的介绍和安装 Zookeeper客户端使用…

基于Paddle2.4.0训练报错Debug

基于Paddle2.4.0训练报错Debug一、环境简介二、Debug过程错误一Debug错误二Debug错误三Debug更换PaddlePaddle版本三、总结参考文档一、环境简介 本文背景为使用AutoDL网站的租借显卡进行网络训练,训练环境如下: 操作系统:Ubuntu18.04&#…

Springboot内置的工具类之StringUtils

在实际的业务开发中,除了经常有针对对象的判断或操作以外,经常也会遇到的就是字符串的判断和操作。比如判断字符串是否为空、是否以某个字符结尾、去除头部和尾部的空白字符、字符的查找和替换。在Spring的核心包中存在这样一个类org.springframework.ut…

如何保证项目如期上线,测试工程师应该怎么做?

要保证项目按照正常进度发布,需要整个研发团队齐心协力。 有很多原因都可能会造成项目延期。1、产品经理频繁修改需求2、开发团队存在技术难题3、测试团队测不完今天我想跟大家聊一下,测试团队如何保证项目按期上线,以及在这个过程中可能遇到…

[附源码]Python计算机毕业设计SSM基于Web课堂签到管理系统(程序+LW)

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

学生HTML网页作业:基于HTML+CSS+JavaScript画家企业8页

🎉精彩专栏推荐 💭文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 💂 作者主页: 【主页——🚀获取更多优质源码】 🎓 web前端期末大作业: 【📚毕设项目精品实战案例 (10…

jvm垃圾处理

1.什么是垃圾 垃圾:没有引用指向的一个对象或者多个对象循环引用但是没有引用指向申请内存方式 c语言:malloc free c:new delete java:new 对象 2、垃圾是如何产生的 垃圾一般在发生引用传递时产生。一块堆内存可以被不同的栈内存所引用,…

DBCO-PEG-SPDP,SPDP-PEG-DBCO,DBCO-聚乙二醇-3-(2-吡啶二硫代)丙酸N-羟基琥珀酰亚胺

一、试剂基团反应特点(Reagent group reaction characteristics): DBCO-PEG-SPDP中无铜点击反应一直是无催化剂生物共轭的有力工具。 DBCO试剂在水性缓冲液中具有稳定性,可用于以高特异性和反应性标记叠氮化物修饰的生物分子&…

【知识图谱】(task3)知识图谱的存储和查询

note 用图数据库的场景: 高性能关系查询:需要快速遍历许多复杂关系的任何用例,如欺诈检测,社交网络分析,网络和数据库基础设施等;模型的灵活性:任何依赖于添加新数据而不会中断现有查询池的用例…

Linux Kernel 6.0 CXL Core Regs.c 详解

前言 CXL 是一个比较新的技术,所以我研究的内核源码是选了当前比较新的内核版本 linux 6.0。打算将内核关于 CXL 的驱动进行解析一遍,一步一步慢慢来。 在阅读之前,希望读者能有一定的 PCIe 基础知识,精力有限,不能把…

C++ 不知算法系列之聊聊希尔、归并排序算法中的分治哲学

1. 前言 排序算法中,冒泡、插入、选择属于相类似的排序算法,这类算法的共同点:通过不停地比较,再使用交换逻辑重新确定数据的位置。 希尔、归并、快速排序算法也可归为同一类,它们的共同点都是建立在分治思想之上。把…