在我的《编译器与解释器的区别和工作原理》一文中已经对编译器和解释器进行了讲述,在开始这个系列的学习之前,建议新手朋友先了解一下这篇文章。

从网上看到了这个系列的文章,感觉很棒,于是把文章的精华整理出来和大家分享。

我并不打算直接翻译原文,而是通过对原文的理解,用自己的方式来阐述文章中的主要内容。

那么,为什么要学习编译器和解释器呢?

按原文中的话来说,编写一个解释器需要综合很多编程技能,并且有效的提高这些技能;而且,还能够帮助我们了解编译器和解释器是如何工作的以及计算机是怎么工作的。

接下来,我们就开始一起来学习如何编写一个简单的解释器。

实际上,我们使用的计算器的程序就是一个解释器,具体来说是一个算术表达式的解释器。

所以,我们可以通过编写一个计算器程序,来初步了解编译器和解释器。

在本文中,我们先来完成加法的运算。

因为我们编写的程序功能要逐步完善,为了保证本文中编写的程序不出错,我们在输入时要注意以下几点:

只能输入个位整数;

只能进行加法;

输入的表达式中不能有空格;

只能输入一次加法运算的表达式,例如:6+9。

一、定义常量

哪些内容是常量呢?

常量包括表达式中的数字类型、运算符以及结束标识。

示例代码:

INTEGER, PLUS, EOF = 'INTEGER', 'PLUS', 'EOF' # 整数,加法,结束标识

二、定义类

都需要定义哪些类呢?

这里,我们需要分析计算器工作的过程。

在用户输入了一个算术表达式之后,我们需要让程序读懂这个表达式,然后进行计算。

表达式的结构是:[数字][运算符][数字]

我们首先要做的应该是将一段表达式中的每个部分取出,然后进行运算处理。

那么,表达式中取出的每一部分,我们都看做是一个记号(Token)。

每个记号都是一个对象,这个对象需要通过类来实例化。

1、记号类(Token)

示例代码:

class Token: # 定义记号类
def __init__(self, value_type, value): # 定义构造方法
self.value_type = value_type # 记号中值的类型
self.value = value # 记号中的值
def __str__(self): # 重写查看记号内容的方法
return 'Token({value_type},{value})'.format(value_type=self.value_type, value=self.value)
def __repr__(self): # 也可以写成 __repr__=__str__
return self.__str__()

在Token类中,除了初始化的构造方法,还有“__str__()”和“__repr__()”这两个魔法方法。

这两个方法都可以用来返回一个可以表示对象的字符串。

区别在于,“__str__()”方法是显示给用户的,“__repr__()”方法是显示给程序开发人员看的。

为了能够理解这个区别,大家可以尝试在Python的交互环境中输入以下代码。

示例代码:

>>>class A:
... def __str__(self):
... return 'str方法的返回值'
... def __repr__(self):
... return 'repr方法的返回值'
>>>a=A()
>>>a
repr方法的返回值
>>>print(a)
str方法的返回值
>>>str(a)
'str方法的返回值'
>>>repr(a)
'repr方法的返回值'

大家能够看到,当我们将类实例化,直接输入对象名称回车后,此时调用的是“__repr__()”方法。

而“print()”方法是给用户友好的显示,通过“print()”方法打印对象,则调用的是“__str__()”方法。

当然,如果我们没有重写“__str__()”方法的话,通过“print()”方法打印对象,则调用的是“__repr__()”方法。

另外,大家还能看到,当使用内置函数“str()”时,调用的是“__str__()”方法。

而使用内置函数“repr()”时,调用的是“__repr__()”方法。

所以,关于这两个方法的重写,当我们想在所有环境中都统一显示内容的话,可以只重写“__repr__()”方法;当我们想在不同环境中有不同显示内容的话(例如:用户和开发人员看到不同的内容),可以分别重写“__str__()”和“__repr__()”方法,实际上“__str__()”方法只是覆盖了“__repr__”方法,以得到更友好的显示内容,呈现给用户。

好了,让我们回归解释器的编写。

2、解释器类(Interpreter)

解释器就像一个工具,具备各种功能,通过它的各种功能实现解释过程。

一个工具是一个对象,他也应该由类的实例化产生。

那么,一个解释器都要包含哪些功能呢?

具体如下:

输入错误时抛出异常;

获取每一个记号;

验证记号流是否符合运算结构,当通过验证,给出计算结果。

示例代码:

class Interpreter: # 定义解释器类
def __init__(self, text): # 定义构造方法获取用户输入的表达式
self.text = text # 用户输入的表达式
self.position = 0 # 获取表达式中每一个字符时的位置
self.current_token = None # 临时保存记号的变量
def error(self): # 定义提示错误的方法
raise Exception('警告:错误的输入内容!') # 抛出异常
def get_next_token(self): # 定义获取记号的方法
text = self.text
if self.position >= len(text): # 如果获取字符的位置已经到达末端
return Token(EOF, None) # 返回结束标识的记号对象
current_char = text[self.position] # 获取当前位置的字符
if current_char.isdigit(): # 如果当前位置的字符是数字
token = Token(INTEGER, int(current_char)) # 实例化整数的记号对象
self.position += 1 # 获取字符的位置自增1,以便获取下一个字符。
return token # 返回记号对象
if current_char == '+': # 如果当前位置的字符是加号
token = Token(PLUS, current_char) # 实例化加号运算符的记号对象
self.position += 1 # 获取字符的位置自增1,以便获取下一个字符。
return token # 返回记号对象
self.error() # 如果以上没有任何对象返回,抛出异常。
def expr(self): # 定义验证运算结构并计算结果的方法
self.current_token = self.get_next_token() # 获取第一个记号
left = self.current_token # 保存第1个记号到变量
if self.current_token.value_type == INTEGER: # 如果记号中的值类型是整数
self.current_token = self.get_next_token() # 获取下一个记号对象存入变量
else: # 否则
self.error() # 抛出异常
operator = self.current_token # 保存第2个记号到变量
if self.current_token.value_type == PLUS: # 如果记号中的值类型是加号
self.current_token = self.get_next_token() # 获取下一个记号对象存入变量
else: # 否则
self.error() # 抛出异常
right = self.current_token # 保存第3个记号到变量
if self.current_token.value_type == INTEGER: # 如果记号中的值类型是整数
self.current_token = self.get_next_token() # 获取下一个记号对象存入变量
else: # 否则
self.error() # 抛出异常
result = left.value + right.value # 进行加法运算获取结果
return result # 返回计算结果
在上方代码中,“expr()”方法是负责运算的方法。
在这个方法中,我们能够看到,它在一个一个的获取表达式中的字符,并进行验证。

如果“left”、“operator”和“right”这3个变量中保存的记号,符合“[数字][运算符][数字]”的运算结构,就进行加法运算,返回结果。

不过在“expr()”方法中,大家能看到很多重复的相类似的语句,也就是标红部分的代码。

这样的代码,我们可以进行抽象处理,把它们独立为一个单独的方法。

这部分代码的功能,主要是验证当前的记号是不是符合运算要求的类型,如果符合要求就“吃掉”当前的记号,获取下一个记号保存到临时保存记号的变量中。

经过抽象后,新的代码如下:

def eat(self, token_type): # 定义辅助运算的方法,此方法用于验证记号对象的值类型是否符合运算要求。
if self.current_token.value_type == token_type: # 如果记号中的值类型符合运算要求
self.current_token = self.get_next_token() # 获取下一个记号对象存入变量
else: # 否则
self.error() # 抛出异常
def expr(self): # 定义验证运算结构并计算结果的方法
self.current_token = self.get_next_token() # 获取第一个记号
left = self.current_token # 保存第1个记号到变量
self.eat(INTEGER) # 调用验证方法传入运算要求的类型
operator = self.current_token # 保存第2个记号到变量
self.eat(PLUS) # 调用验证方法传入运算要求的类型
right = self.current_token # 保存第3个记号到变量
self.eat(INTEGER) # 调用验证方法传入运算要求的类型
result = left.value + right.value # 进行加法运算获取结果
return result # 返回计算结果

三、定义主函数并执行

示例代码:

def main():
while True: # 循环获取输入
try:
text = input('>>>') # 获取用户输入
except EOFError: # 捕获到末端错误时退出
break
if not text: # 如果未输入时继续提示输入
continue
interpreter = Interpreter(text) # 实例化解释器对象
result = interpreter.expr() # 执行运算方法获取运算结果
print(text, '=', result) #
if __name__ == '__main__':
main()

到这里我们就完成了一个初步的能够解释加法表达式的解释器。

最后,我们需要掌握一些概念。

将输入的字符串表达式切分为记号的过程叫做词法分析。

解释器工作时,第一步就需要读取输入内容,并能够将内容转换为一系列的记号。完成这部分工作的组件叫做词法分析器(Lexer:Lexical Ananlyzer的简称),也可以叫做扫描器(Scanner)或者分词器(Tokenizer)。

以上就是《一起来写一个简单的解释器》系列文章的第一篇内容。

在这篇内容的基础上,大家可以尝试进行以下功能的扩展:

让解释器支持减法运算;

让解释器支持多位整数的运算,例如:12+36;

添加一个方法,让解释器能够处理用户所输入表达式中的空格。

而且,当学习完这篇文章,请大家自我检查一下,是否了解了以下内容:

什么是解释器?

什么是编译器?

解释器和编译器的区别是什么?

什么是记号(Token)?

将输入内容切分为记号的过程叫什么?

解释器工作时,负责词法分析的组件叫什么?

词法分析的组件还有哪些其它的常用名称?