讓我們做個簡單的解釋器(二)
![](https://linuxstory.org/wp-content/uploads/2024/06/103057jm2ojoccjg3jj3ey-800x400.jpg)
在一本叫做 《高效思考的 5 要素》 的書中,作者 Burger 和 Starbird 講述了一個關於他們如何研究 Tony Plog 的故事,他是一位舉世聞名的交響曲名家,為一些有才華的演奏者開創了一個大師班。這些學生一開始演奏複雜的樂曲,他們演奏的非常好。然後他們被要求演奏非常基礎簡單的樂曲。當他們演奏這些樂曲時,與之前所演奏的相比,聽起來非常幼稚。在他們結束演奏後,老師也演奏了同樣的樂曲,但是聽上去非常嫻熟。差別令人震驚。Tony 解釋道,精通簡單音符可以讓人更好的掌握複雜的部分。這個例子很清晰 —— 要成為真正的名家,必須要掌握簡單基礎的思想。
故事中的例子明顯不僅僅適用於音樂,而且適用於軟體開發。這個故事告訴我們不要忽視繁瑣工作中簡單基礎的概念的重要性,哪怕有時候這讓人感覺是一種倒退。儘管熟練掌握一門工具或者框架非常重要,了解它們背後的原理也是極其重要的。正如 Palph Waldo Emerson 所說:
「如果你只學習方法,你就會被方法束縛。但如果你知道原理,就可以發明自己的方法。」
今天我會向你們展示一個全新的計算器,與 第一部分 相比,它可以做到:
- 處理輸入字元串任意位置的空白符
- 識別輸入字元串中的多位整數
- 做兩個整數之間的減法(目前它僅能加減整數)
新版本計算器的源代碼在這裡,它可以做到上述的所有事情:
# 標記類型
# EOF (end-of-file 文件末尾)標記是用來表示所有輸入都解析完成
INTEGER, PLUS, MINUS, EOF = 'INTEGER', 'PLUS', 'MINUS', 'EOF'
class Token(object):
def __init__(self, type, value):
# token 類型: INTEGER, PLUS, MINUS, or EOF
self.type = type
# token 值: 非負整數值, '+', '-', 或無
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):
# 客戶端字元輸入, 例如. "3 + 5", "12 - 5",
self.text = text
# self.pos 是 self.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 # Indicates end of input
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):
# 將當前的標記類型與傳入的標記類型作比較,如果他們相匹配,就
# 「eat」 掉當前的標記並將下一個標記賦給 self.current_token,
# 否則拋出一個異常
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)
# 在上述函數調用後,self.current_token 就被設為 EOF 標記
# 這時要麼是成功地找到 INTEGER PLUS INTEGER,要麼是 INTEGER MINUS INTEGER
# 序列的標記,並且這個方法可以僅僅返回兩個整數的加或減的結果,就能高效解釋客戶端的輸入
if op.type == PLUS:
result = left.value + right.value
else:
result = left.value - right.value
return result
def main():
while True:
try:
# To run under Python3 replace 'raw_input' call
# with 'input'
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 上下載。試著運行它。看看它是不是正常工作:它應該能夠處理輸入中任意位置的空白符;能夠接受多位的整數,並且能夠對兩個整數做減法和加法。
這是我在自己的筆記本上運行的示例:
$ python calc2.py
calc> 27 + 3
30
calc> 27 - 7
20
calc>
與 第一部分 的版本相比,主要的代碼改動有:
get_next_token
方法重寫了很多。增加指針位置的邏輯之前是放在一個單獨的方法中。- 增加了一些方法:
skip_whitespace
用於忽略空白字元,integer
用於處理輸入字元的多位整數。 expr
方法修改成了可以識別 「整數 -> 減號 -> 整數」 片語和 「整數 -> 加號 -> 整數」 片語。在成功識別相應的片語後,這個方法現在可以解釋加法和減法。
第一部分 中你學到了兩個重要的概念,叫做 標記 和 詞法分析 。現在我想談一談 詞法 、 解析 和 解析器 。
你已經知道了標記。但是為了讓我詳細的討論標記,我需要談一談詞法。詞法是什麼? 詞法 是一個 標記 中的字元序列。在下圖中你可以看到一些關於標記的例子,這可以讓它們之間的關係變得清晰:
現在還記得我們的朋友,expr
方法嗎?我之前說過,這是數學表達式實際被解釋的地方。但是你要先識別這個表達式有哪些片語才能解釋它,比如它是加法還是減法。expr
方法最重要的工作是:它從 get_next_token
方法中得到流,並找出該標記流的結構,然後解釋已經識別出的片語,產生數學表達式的結果。
在標記流中找出結構的過程,或者換種說法,識別標記流中的片語的過程就叫 解析 。解釋器或者編譯器中執行這個任務的部分就叫做 解析器 。
現在你知道 expr
方法就是你的解釋器的部分, 解析 和 解釋 都在這裡發生 —— expr
方法首先嘗試識別(解析)標記流里的 「整數 -> 加法 -> 整數」 或者 「整數 -> 減法 -> 整數」 片語,成功識別後 (解析了) 其中一個片語,這個方法就開始解釋它,返回兩個整數的和或差。
又到了練習的時間。
- 擴展這個計算器,讓它能夠計算兩個整數的乘法
- 擴展這個計算器,讓它能夠計算兩個整數的除法
- 修改代碼,讓它能夠解釋包含了任意數量的加法和減法的表達式,比如 「9 - 5 + 3 + 11」
檢驗你的理解:
- 詞法是什麼?
- 找出標記流結構的過程叫什麼,或者換種說法,識別標記流中一個片語的過程叫什麼?
- 解釋器(編譯器)執行解析的部分叫什麼?
希望你喜歡今天的內容。在該系列的下一篇文章里你就能擴展計算器從而處理更多複雜的算術表達式。敬請期待。
via: https://ruslanspivak.com/lsbasi-part2/
作者:Ruslan Spivak 譯者:BriFuture 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive