原文:Let’s Build A Simple Interpreter. Part 3.
译文:
今天早上醒来后,我在想:“为什么我们觉得学习一项新技能这么难?”
我认为,这不仅仅是因为学习本身很难。一个重要原因可能是我们花了大量时间和精力通过阅读和观看获取知识,却没有足够的时间通过练习将知识转化为技能。以游泳为例。你可以花很多时间读几百本关于游泳的书,与有经验的游泳运动员和教练交流几个小时,观看所有的训练视频,但当你第一次跳进泳池时,还是会像石头一样沉下去。
归根结底的原因是:你认为自己对这门学科的了解程度并不重要——你必须将这些知识运用到实践中,才能将其转化为技能。为了帮助你练习,我在本系列的第一部分 和第二部分 中增加了练习。没错,你会在今天的文章和以后的文章中看到更多的练习,我向你保证:)
好了,我们开始讲今天的内容吧?
到目前为止,你已经学会了如何解释两个整数加减法的算术表达式,比如“7+3”或“12-9”。今天我要讲的是如何解析(识别)和解释包含任意数量加减运算符的算术表达式,例如“7-3+2-1”。
从图形上看,本文中的算术表达式可以用下面的语法图来表示:
语法图 什么是语法图?语法图 是一种编程语言的语法规则的图形化表示。语法图可以直观地告诉你哪些语句在你的编程语言中是允许的,哪些是不允许的。
语法图读起来很容易:只要按照箭头所示的路径走就可以了。有些路径表示选择,有些路径表示循环。
你可以把上面的语法图理解
为:一个术语后面有一个加号或减号,后面是另一个术语,而这个术语后面又有一个加号或减号,然后是另一个术语,以此类推。用文字来表述就是这个意思。你可能想知道什么是“术语”。在本文中,“术语”只是一个整数。
语法图有两个主要作用:
它们以图示的形式表示了一种编程语言的规范(语法)。
它们可以用来帮助你编写解析器——你可以按照简单的规则将图映射成代码。
你已经知道了,在标记流中识别一个短语的过程被称为解析 。而解释器或编译器中执行这项工作的部分被称为解析器 。解析也被称为语法分析 ,解析器也因此被称为语法分析器 。
根据上面的语法图,下面的算术表达式都是有效的:
因为不同编程语言中的算术表达式的语法规则非常相似,我们可以用Python shell来“测试”我们的语法图。启动你的Python shell,自己去看看。
1 2 3 4 5 6 >>> 3 3 >>> 3 + 4 7 >>> 7 - 3 + 2 - 1 5
结果和我们预期的一样。
“3+”这个表达式并不是一个有效的算术表达式,因为根据语法图,加号后面必须有一个术语(整数),否则就是语法错误。还是那句话,用Python shell试试,你自己试试就知道了。
1 2 3 4 5 >>> 3 + File "<stdin>" , line 1 3 + ^ SyntaxError: invalid syntax
能够用Python shell做一些测试当然很好,但是让我们把上面的语法图映射到代码中,用我们自己的解释器来做测试,怎么样?
你从前面的文章(第1部分 和第2部分 )中知道,我们把解析器和解释器都放到了expr方法里面。再重述一下,解析器只是识别结构,确保它符合某些规范,而解释器在解析器成功识别(解析)了表达式之后,再对表达式进行求值。
解析器与解释器 下面的代码片段显示了与图中对应的解析器代码。语法图中的矩形框(术语 )变成了解析整数的term方法,而expr 方法只是按照语法图的流程进行操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def term (self ): self.eat(INTEGER) def expr (self ): self.current_token = self.get_next_token() self.term() while self.current_token.type in (PLUS, MINUS): token = self.current_token if token.type == PLUS: self.eat(PLUS) self.term() elif token.type == MINUS: self.eat(MINUS) self.term()
你可以看到,expr 首先调用term 方法。然后expr 方法有一个while 循环,这个循环可以执行0次或多次。在这个循环中,解析器会根据标记(是加号还是减号)做出选择。花点时间研究一下,确保上面的代码确实是遵循了算术表达式的语法图流程。
但解析器本身并不解释任何东西:如果它识别出一个表达式,它什么也不干,如果不行,它就会抛出一个语法错误。让我们修改一下expr 方法并添加解释器代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 def term (self ): """返回一个INTEGER标记值""" token = self.current_token self.eat(INTEGER) return token.value def expr (self ): """解析器/解释器""" self.current_token = self.get_next_token() result = self.term() while self.current_token.type in (PLUS, MINUS): token = self.current_token if token.type == PLUS: self.eat(PLUS) result = result + self.term() elif token.type == MINUS: self.eat(MINUS) result = result - self.term() return result
因为解释器需要对一个表达式求值,所以我们修改了term方法,让它返回一个整数值,并也修改了expr方法,在需要的地方执行加减法,返回解释的结果。虽然这段代码相当简单明了,但我建议花点时间研究一下。
现在就开始行动起来,看看解释器的完整代码。
这里是你的新版计算器的源代码,可以处理包含整数和任意数量的加减法运算符的有效算术表达式。 将上述代码保存到calc3.py文件中,或者直接从GitHub 中下载。试用一下。看看它能不能处理我之前给你看的语法图中的算术表达式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 INTEGER, PLUS, MINUS, EOF = 'INTEGER' , 'PLUS' , 'MINUS' , 'EOF' class Token (object ): def __init__ (self, type , value ): self.type = type self.value = value def __str__ (self ): """类实例的字符串表示。 例子: Token(INTEGER, 3) Token(PLUS '+') """ return 'Token({type}, {value})' .format ( type =self.type , value=repr (self.value) ) def __repr__ (self ): return self.__str__() class Interpreter (object ): def __init__ (self, text ): self.text = text self.pos = 0 self.current_token = None self.current_char = self.text[self.pos] def error (self ): raise Exception('Invalid syntax' ) def advance (self ): """前移'pos'指针,设置'current_char'变量。""" self.pos += 1 if self.pos > len (self.text) - 1 : self.current_char = None else : self.current_char = self.text[self.pos] def skip_whitespace (self ): while self.current_char is not None and 她她current_char.isspace(): self.advance() def integer (self ): """返回从输入中处理过来的(多位数)整数。""" result = '' while self.current_char is not None and 她current_char.isdigit(): result += self.current_char self.advance() return int (result) def get_next_token (self ): """词法分析器(也叫扫描器或分词器) 此方法负责将句子拆分成一个个标记。一次一个标记。 """ while self.current_char is not None : if selfcurrent_char.isspace(): self.skip_whitespace() continue if selfcurrent_char.isdigit(): return Token(INTEGER, self.integer()) if selfcurrent_char == '+' : self.advance() return Token(PLUS, '+' ) if selfcurrent_char == '-' : self.advance() return Token(MINUS, '-' ) self.error() return Token(EOF, None ) def eat (self, token_type ): if self.current_token.type == token_type: self.current_token = self.get_next_token() else : self.error() def term (self ): """返回一个INTEGER标记值。""" token = self.current_token self.eat(INTEGER) return token.value def expr (self ): """算术表达式解析器/解释器。""" self.current_token = self.get_next_token() result = self.term() while self.current_token.type in (PLUS, MINUS): token = self.current_token if token.type == PLUS: self.eat(PLUS) result = result + self.term() elif token type == MINUS: self.eat(MINUS) result = result - self.term() return result def main (): while True : try : ,使用“input ”代替“raw_input” text = raw_input('calc> ' ) except EOFError: break if not text: continue interpreter = Interpreter(text) result = interpreter.expr() print (result) if __name__ == '__main__' : main()
下面是我在笔记本上运行的情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 $ python calc3.py calc> 3 3 calc> 7 - 4 3 calc> 10 + 5 15 calc> 7 - 3 + 2 - 1 5 calc> 10 + 1 + 2 - 3 + 4 + 6 - 15 5 calc> 3 + Traceback (most recent call last): File "calc3.py" , line 147, in <module> main() File "calc3.py" , line 142, in main result = interpreter.expr() File "calc3.py" , line 123, in expr result = result + self.term() File "calc3.py" , line 110, in term self.eat(INTEGER) File "calc3.py" , line 105, in eat self.error() File "calc3.py" , line 45, in error raise Exception('Invalid syntax' ) Exception: Invalid syntax
还记得我在文章开头提到的那些练习吗?:)
画一个只包含乘法和除法的算术表达式的语法图,例如“74/2 3”。说真的,拿起笔或铅笔,试着画一个就可以了。
修改计算器的源代码,以解释只包含乘法和除法的算术表达式,例如“7 * 4 / 2 * 3”。
从头开始写一个解释器来处理像“7 - 3 + 2 - 1”这样的算术表达式。使用任何你喜欢的编程语言,不看例子,凭空写出来。当你这样做的时候,请考虑一下所涉及的组件:一个分词器,它接收一个输入并将其转换为标记流,一个解析器,它从分词器获取信息,并将其转化成标记流,以及一个解释器,在解析器成功解析(识别)了一个有效的算术表达式后,生成结果。把这些部分串起来。花点时间把你所学到的知识转化为一个可以使用的算术表达式解释器。
检查你是否完全理解了
什么是语法图?
什么是语法分析?
什么是语法分析器?
嘿,你看!你一直读到最后。谢谢你今天来过,别忘了做练习。我下次再来的时候会有新的文章——敬请关注。
下面是我推荐的书单,它们对你学习解释器和编译器非常有帮助。
Language Implementation Patterns: Create Your Own Domain-Specific and General Programming Languages (Pragmatic Programmers)
Writing Compilers and Interpreters: A Software Engineering Approach
Modern Compiler Implementation in Java
Modern Compiler Design
Compilers: Principles, Techniques, and Tools (2nd Edition)