作者 | Ruslan Spivak
譯者 | 周家未 (BriFuture) ? ? ? 共計翻譯:21 篇 貢獻時間:611 天
在一本叫做 《高效思考的 5 要素》 的書中,作者 Burger 和 Starbird 講述了一個關於他們如何研究 Tony Plog 的故事,他是一位舉世聞名的交響曲名家,為一些有才華的演奏者開創了一個大師班。這些學生一開始演奏複雜的樂曲,他們演奏的非常好。然後他們被要求演奏非常基礎簡單的樂曲。當他們演奏這些樂曲時,與之前所演奏的相比,聽起來非常幼稚。在他們結束演奏後,老師也演奏了同樣的樂曲,但是聽上去非常嫻熟。差別令人震驚。Tony 解釋道,精通簡單音符可以讓人更好的掌握複雜的部分。這個例子很清晰 —— 要成為真正的名家,必須要掌握簡單基礎的思想。
故事中的例子明顯不僅僅適用於音樂,而且適用於軟體開發。這個故事告訴我們不要忽視繁瑣工作中簡單基礎的概念的重要性,哪怕有時候這讓人感覺是一種倒退。儘管熟練掌握一門工具或者框架非常重要,瞭解它們背後的原理也是極其重要的。正如 Palph Waldo Emerson 所說:
“如果你只學習方法,你就會被方法束縛。但如果你知道原理,就可以發明自己的方法。”
有鑒於此,讓我們再次深入瞭解直譯器和編譯器。
今天我會向你們展示一個全新的計算器,與 第一部分[1] 相比,它可以做到:
新版本計算器的原始碼在這裡,它可以做到上述的所有事情:
# 標記型別
# 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[2] 上下載。試著執行它。看看它是不是正常工作:它應該能夠處理輸入中任意位置的空白符;能夠接受多位的整數,並且能夠對兩個整數做減法和加法。
這是我在自己的筆記本上執行的示例:
$ python calc2.py
calc> 27 + 3
30
calc> 27 - 7
20
calc>
與 第一部分[1] 的版本相比,主要的程式碼改動有:
get_next_token
方法重寫了很多。增加指標位置的邏輯之前是放在一個單獨的方法中。skip_whitespace
用於忽略空白字元,integer
用於處理輸入字元的多位整數。expr
方法修改成了可以識別 “整數 -> 減號 -> 整數” 片語和 “整數 -> 加號 -> 整數” 片語。在成功識別相應的片語後,這個方法現在可以解釋加法和減法。第一部分[1] 中你學到了兩個重要的概念,叫做 標記 和詞法分析。現在我想談一談詞法、 解析 和解析器。
你已經知道了標記。但是為了讓我詳細的討論標記,我需要談一談詞法。詞法是什麼?詞法是一個標記中的字元序列。在下圖中你可以看到一些關於標記的例子,這可以讓它們之間的關係變得清晰:
現在還記得我們的朋友,expr
方法嗎?我之前說過,這是數學運算式實際被解釋的地方。但是你要先識別這個運算式有哪些片語才能解釋它,比如它是加法還是減法。expr
方法最重要的工作是:它從 get_next_token
方法中得到流,並找出該標記流的結構,然後解釋已經識別出的片語,產生數學運算式的結果。
在標記流中找出結構的過程,或者換種說法,識別標記流中的片語的過程就叫解析。直譯器或者編譯器中執行這個任務的部分就叫做解析器。
現在你知道 expr
方法就是你的直譯器的部分,解析和解釋都在這裡發生 —— expr
方法首先嘗試識別(解析)標記流裡的 “整數 -> 加法 -> 整數” 或者 “整數 -> 減法 -> 整數” 片語,成功識別後 (解析了) 其中一個片語,這個方法就開始解釋它,傳回兩個整數的和或差。
又到了練習的時間。
檢驗你的理解:
希望你喜歡今天的內容。在該系列的下一篇文章裡你就能擴充套件計算器從而處理更多複雜的算術運算式。敬請期待。
via: https://ruslanspivak.com/lsbasi-part2/
作者:Ruslan Spivak[4] 譯者:BriFuture 校對:wxy
本文由 LCTT 原創編譯,Linux中國 榮譽推出