本系列文章参考国外编程高手鲁斯兰的博客文章《Let’s Build A Simple Interpreter》。
上一篇文章中,我们一起实现了计算器的加法功能。
并且,在文末给大家留下了一些练习:
- 让解释器支持减法运算;
- 让解释器支持多位整数的运算,例如:12+36;
- 添加一个方法,让解释器能够处理用户所输入表达式中的空格。
我建议大家先独自尝试完成这些练习,再继续本篇文章的学习。
当然,如果你如果已经尽力,但是没能够完成练习,那么,这篇文章将会给你带来很大的收获。
在开始这篇文章的主体内容之前,我先引用原文中的一个小故事。
这个故事它所包含的哲理适用于各行各业。
《高效思考的5个要素》的作者 Burger 和 Starbird 在书中分享了一个关于他们观看国际知名小号演奏家 Tony Plog 为多才多艺的小号演奏者举办大师课的故事。学生们首先演奏了复杂的音乐章节,他们表演的都很棒。但是,随后他们被要求表演非常基础、简单的音符。当他们演奏那些音符时,比起之前演奏的复杂乐章显得有些稚嫩。当他们完成演奏之后,大师也演奏了相同的音符。但大师演奏这些音符时听起来并不稚嫩,差异非常明显。Tony 解释道:掌握简单音符的演奏,让演奏者可以更完美地掌握复杂的乐章。
这个故事的结论:要想成为真正的艺术家,你必须集中精力掌握简单、基础的概念。
再精密的仪器也都是由简单的一个一个零件巧妙的组合而成。
在任何一个领域,你必须先掌握这些简单基础的概念,再通过灵活的运用才能够成为这个领域的高手。
接下来,我们再继续依靠我们所学过的Python3基础知识,来为我们的计算器或者说算术表达式的解释器添加更多的功能。
根据之前的练习要求,我们逐步来完成这些任务。
一、增加保存当前字符的变量
因为我们是逐个获取字符进行验证,一个多位数字需要多次获取才能够组成完整的数字生成记号,那么在记号生成之前,我们需要先使用一个变量保存字符,进行验证。
示例代码:(class Interpreter)
def __init__(self, text): ...省略其它代码... self.current_char = self.text[self.position] # 设置当前字符为指定位置的字符
二、增加获取下一个字符的方法
因为新增加的功能中表达式中字符的数量不再是3个,而是无法固定的数量。
数字部分可能是多位数字,并且表达式中可能包含任意数量的空格。
这样的话,我们需要对字符进行循环验证,会经常用到获取下一个字符的方法。
那么,经常用到的重复的方法,我们把它抽象成一个独立的方法。
示例代码:
def advance(self): # 定义获取下一个字符的方法 self.position += 1 # 获取字符的位置自增 if self.position >= len(self.text): # 如果位置到达字符串的末尾 self.current_char = None # 设置当前字符为None值 else: # 否则 self.current_char = self.text[self.position] # 设置当前字符为指定位置的字符
三、增加跳过空格的方法
如果遇到空格,无论是单个还是连续多个,我们都应该将其跳过。
示例代码:
def skip_whitespace(self): # 定义跳过空格的方法 while self.current_char is not None and self.current_char.isspace(): # 如果当前字符不是None值并且当前字符是空格 self.advance() # 获取下一个字符
四、增加获取多位数字的方法
如果获取到的字符是数字,无论是单个还是连续多个,我们都将它们连接起来,转为整数类型后返回。
示例代码:
def long_integer(self): # 获取多位数字 result = '' while self.current_char is not None and self.current_char.isdigit(): # 如果当前字符不是None值并且当前字符是数字 result += self.current_char # 连接数字 self.advance() # 获取下一个字符 return int(result) # 返回数字
五、修改词法分析器的代码
在新版本的词法分析器中,我们只需要根据当前字符的类型进行不同的处理。
示例代码:
def get_next_token(self): while self.current_char is not None: # 如果当前字符不是None值 if self.current_char.isspace(): # 如果当前字符是空格 self.skip_whitespace() # 跳过所有空格 continue if self.current_char.isdigit(): # 如果当前字符是整数 return Token(INTEGER, self.long_integer()) # 获取完整的数字创建记号对象并返回 if self.current_char == '+': # 如果当前字符是加号 self.advance() # 跳到下一字符 return Token(PLUS, self.current_char) # 创建记号对象并返回 if self.current_char == '-': # 如果当前字符是减号 self.advance() # 跳到下一字符 return Token(MINUS, self.current_char) # 创建记号对象并返回 self.error() # 如果以上都不是,则抛出异常。 return Token(EOF, None) # 遍历结束返回结束标识创建的记号对象
这一次,词法分析器的代码更加清晰。
六、修改表达式计算方法
这里我们添加减法的支持。
def expr(self): self.current_token = self.get_next_token() left = self.current_token self.eat(INTEGER) operator = self.current_token if operator.value_type == PLUS: # 如果运算符的类型是加法 self.eat(PLUS) # 验证加法运算符 else: # 否则 self.eat(MINUS) # 验证减法运算符 right = self.current_token self.eat(INTEGER) if operator.value_type == PLUS: # 如果运算符的类型是加法 result = left.value + right.value # 进行加法运算 else: # 否则 result = left.value - right.value # 进行减法运算 return result
上方代码中,添加注释的部分是更新的内容。
到这里,我们就完成了所有目标功能。
最后,我们仍然需要掌握一些概念。
在上一篇文章中,我们了解了在两个重要的概念: Token(记号) 和 Lexical Analyzer(词法分析器)。
这一篇文章中,我们来简单了解一下 lexeme(词位)、parsing(语法分析)和 parser(语法分析器)。
1、 lexeme(词位)
词位的中文解释是语言词汇的基本单位。
在这里,lexeme就是组成 token 的字符序列。
以当前的案例来说,词位就是数字、加号和减号:
记号 | 词位 |
INTEGER | 1,12,666,69,8,0,3568 |
PLUS | + |
MINUS | – |
2、parsing(语法分析)和 parser(语法分析器)。
我们所编写的代码中,“ expr()”方法是真正的对一个算术表达式进行解释的地方。
但是,在对一个算术表达式进行解释之前,我们需要先识别短语的类型,比如是加法表达式还是减法表达式。
“ expr()”方法本质上就是:先从“ get_next_token()” 方法获取token流,找到token流中的特定的结构,或者说识别出特定的短语,然后,解释识别出的短语,生成算术表达式的结果。
找到token流中特定结构的过程,或者说,识别token 流中特定短语的过程称之为Parsing(语法分析)。
解释器或编译器中完成语法分析的部分,叫做Parser(语法分析器)。
现在我们知道“ expr()”方法是解释器中既做了 Parsing(语法分析),又做了Interpreting(解释)的部分。
“ expr()”方法首先尝试识别(Parsing)token流中的[INTEGER>>PLUS>>INTEGER]结构或者[INTEGER>>MINUS>>INTEGER] 结构的短语,成功识别其中一种短语后,对短语进行解释,并将两整数相加或相减的结果返回。
以上就是《一起来写一个简单的解释器》系列文章的第二篇内容。
在这篇内容的基础上,大家可以尝试进行以下功能的扩展:
- 让解释器支持乘法运算;
- 让解释器支持除法运算;
- 让解释器支持任意多加法和减法运算,例如:3+7-4-2+8。
而且,当学习完这篇文章,请大家自我检查一下,是否了解了以下内容:
- 什么是 Lexeme(词位)?
- 找出Token流中特定结构的过程叫什么?或者说,从 Token流中识别出特定短语的过程叫什么?
- 解释器或编译器中做 parsing(语法分析)部分的叫什么?
项目源代码下载:【点此下载】
转载请注明:魔力Python » 一起来写个简单的解释器(2)