在一本叫做 《高效思考的 5 要素》 的书中,作者 Burger 和 Starbird 讲述了一个关于他们如何研究 Tony Plog 的故事,他是一位举世闻名的交响曲名家,为一些有才华的演奏者开创了一个大师班。这些学生一开始演奏复杂的乐曲,他们演奏的非常好。然后他们被要求演奏非常基础简单的乐曲。当他们演奏这些乐曲时,与之前所演奏的相比,听起来非常幼稚。在他们结束演奏后,老师也演奏了同样的乐曲,但是听上去非常娴熟。差别令人震惊。Tony 解释道,精通简单音符可以让人更好的掌握复杂的部分。这个例子很清晰 —— 要成为真正的名家,必须要掌握简单基础的思想。
故事中的例子明显不仅仅适用于音乐,而且适用于软件开发。这个故事告诉我们不要忽视繁琐工作中简单基础的概念的重要性,哪怕有时候这让人感觉是一种倒退。尽管熟练掌握一门工具或者框架非常重要,了解它们背后的原理也是极其重要的。正如 Palph Waldo Emerson 所说:
“如果你只学习方法,你就会被方法束缚。但如果你知道原理,就可以发明自己的方法。”
有鉴于此,让我们再次深入了解解释器和编译器。
今天我会向你们展示一个全新的计算器,与 第一部分 相比,它可以做到:
处理输入字符串任意位置的空白符
识别输入字符串中的多位整数
做两个整数之间的减法(目前它仅能加减整数)
新版本计算器的源代码在这里,它可以做到上述的所有事情:
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 149 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 ): """String representation of the class instance. Examples: 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('Error parsing input' ) def advance (self ): """Advance the 'pos' pointer and set the 'current_char' variable.""" 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 self .current_char.isspace(): self .advance() def integer (self ): """Return a (multidigit) integer consumed from the input.""" result = '' while self .current_char is not None and self .current_char.isdigit(): result += self .current_char self .advance() return int (result) def get_next_token (self ): """Lexical analyzer (also known as scanner or tokenizer) This method is responsible for breaking a sentence apart into tokens. """ while self .current_char is not None : if self .current_char.isspace(): self .skip_whitespace() continue if self .current_char.isdigit(): return Token(INTEGER, self .integer()) if self .current_char == '+' : self .advance() return Token(PLUS, '+' ) if self .current_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 expr (self ): """Parser / Interpreter expr -> INTEGER PLUS INTEGER expr -> INTEGER MINUS INTEGER """ self .current_token = self .get_next_token() left = self .current_token self .eat(INTEGER) op = self .current_token if op.type == PLUS: self .eat(PLUS) else : self .eat(MINUS) right = self .current_token self .eat(INTEGER) if op.type == PLUS: result = left.value + right.value else : result = left.value - right.value return resultdef main (): while True : try : text = raw_input('calc> ' ) except EOFError: break if not text: continue interpreter = Interpreter(text) result = interpreter.expr() print (result)if __name__ == '__main__' : main()
把上面的代码保存到 calc2.py
文件中,或者直接从 GitHub 上下载。试着运行它。看看它是不是正常工作:它应该能够处理输入中任意位置的空白符;能够接受多位的整数,并且能够对两个整数做减法和加法。
这是我在自己的笔记本上运行的示例:
1 2 3 4 5 6 7 $ python calc2.py calc> 27 + 3 30calc> 27 - 7 20calc>
与 第一部分 的版本相比,主要的代码改动有:
get_next_token
方法重写了很多。增加指针位置的逻辑之前是放在一个单独的方法中。
增加了一些方法:skip_whitespace
用于忽略空白字符,integer
用于处理输入字符的多位整数。
expr
方法修改成了可以识别 “整数 -> 减号 -> 整数” 词组和 “整数 -> 加号 -> 整数” 词组。在成功识别相应的词组后,这个方法现在可以解释加法和减法。
第一部分 中你学到了两个重要的概念,叫做 标记 token 和 词法分析 lexical analyzer 。现在我想谈一谈 词法 lexeme 、 解析 parsing 和 解析器 parser 。
你已经知道了标记。但是为了让我详细的讨论标记,我需要谈一谈词法。词法是什么? 词法 lexeme 是一个 标记 token 中的字符序列。在下图中你可以看到一些关于标记的例子,这可以让它们之间的关系变得清晰:
现在还记得我们的朋友,expr
方法吗?我之前说过,这是数学表达式实际被解释的地方。但是你要先识别这个表达式有哪些词组才能解释它,比如它是加法还是减法。expr
方法最重要的工作是:它从 get_next_token
方法中得到流,并找出该标记流的结构,然后解释已经识别出的词组,产生数学表达式的结果。
在标记流中找出结构的过程,或者换种说法,识别标记流中的词组的过程就叫 解析 parsing 。解释器或者编译器中执行这个任务的部分就叫做 解析器 parser 。
现在你知道 expr
方法就是你的解释器的部分, 解析 parsing 和 解释 interpreting 都在这里发生 —— expr
方法首先尝试识别(解析)标记流里的 “整数 -> 加法 -> 整数” 或者 “整数 -> 减法 -> 整数” 词组,成功识别后 (解析了) 其中一个词组,这个方法就开始解释它,返回两个整数的和或差。
又到了练习的时间。
扩展这个计算器,让它能够计算两个整数的乘法
扩展这个计算器,让它能够计算两个整数的除法
修改代码,让它能够解释包含了任意数量的加法和减法的表达式,比如 “9 - 5 + 3 + 11”
检验你的理解:
词法是什么?
找出标记流结构的过程叫什么,或者换种说法,识别标记流中一个词组的过程叫什么?
解释器(编译器)执行解析的部分叫什么?
希望你喜欢今天的内容。在该系列的下一篇文章里你就能扩展计算器从而处理更多复杂的算术表达式。敬请期待。
via: https://ruslanspivak.com/lsbasi-part2/
作者:Ruslan Spivak 译者:BriFuture 校对:wxy
本文由 LCTT 原创编译,Linux中国 荣誉推出