作者 | Ruslan Spivak
譯者 | 周家未 (BriFuture) ? ? ? 共計翻譯:21 篇 貢獻時間:611 天
早上醒來的時候,我就在想:“為什麼我們學習一個新技能這麼難?”
我不認為那是因為它很難。我認為原因可能在於我們花了太多的時間,而這件難事需要有豐富的閱歷和足夠的知識,然而我們要把這樣的知識轉換成技能所用的練習時間又不夠。
拿游泳來說,你可以花上幾天時間來閱讀很多有關游泳的書籍,花幾個小時和資深的游泳者和教練交流,觀看所有可以獲得的訓練影片,但你第一次跳進水池的時候,仍然會像一個石頭那樣沉入水中,
要點在於:你認為自己有多瞭解那件事都無關緊要 —— 你得透過練習把知識變成技能。為了幫你練習,我把訓練放在了這個系列的 第一部分[1] 和 第二部分[2] 了。當然,你會在今後的文章中看到更多練習,我保證 :)
好,讓我們開始今天的學習。
到現在為止,你已經知道了怎樣解釋像 “7 + 3” 或者 “12 – 9” 這樣的兩個整數相加減的算術運算式。今天我要說的是怎麼解析(識別)、解釋有多個數字相加減的算術運算式,比如 “7 – 3 + 2 – 1”。
文中的這個算術運算式可以用下麵的這個語法圖表示:
什麼是語法圖? 語法圖 是對一門程式語言中的語法規則進行影象化的表示。基本上,一個語法圖就能告訴你哪些陳述句可以在程式中出現,哪些不能出現。
語法圖很容易讀懂:按照箭頭指向的路徑。某些路徑表示的是判斷,有些表示的是迴圈。
你可以按照以下的方式讀上面的語法圖:一個 term 後面可以是加號或者減號,接著可以是另一個 term,這個 term 後面又可以是一個加號或者減號,後面又是一個 term,如此迴圈。從字面上你就能讀懂這個圖片了。或許你會奇怪,“term” 是什麼、對於本文來說,“term” 就是個整數。
語法圖有兩個主要的作用:
你已經知道,識別出記號流中的片語的過程就叫做 解析。直譯器或者編譯器執行這個任務的部分叫做 解析器。解析也稱為 語法分析,並且解析器這個名字很合適,你猜的對,就是 語法分析器。
根據上面的語法圖,下麵這些運算式都是合法的:
因為算術運算式的語法規則在不同的程式語言裡面是很相近的,我們可以用 Python shell 來“測試”語法圖。開啟 Python shell,執行下麵的程式碼:
>>> 3
3
>>> 3 + 4
7
>>> 7 - 3 + 2 - 1
5
意料之中。
運算式 “3 + ” 不是一個有效的數學運算式,根據語法圖,加號後面必須要有個 term (整數),否則就是語法錯誤。然後,自己在 Python shell 裡面執行:
>>> 3 +
File "
" , line 1
3 +
^
SyntaxError: invalid syntax
能用 Python shell 來做這樣的測試非常棒,讓我們把上面的語法圖轉換成程式碼,用我們自己的直譯器來測試,怎麼樣?
從之前的文章裡(第一部分[1] 和 第二部分[2])你知道 expr
方法包含了我們的解析器和直譯器。再說一遍,解析器僅僅識別出結構,確保它與某些特性對應,而直譯器實際上是在解析器成功識別(解析)特性之後,就立即對運算式進行評估。
以下程式碼片段顯示了對應於圖表的解析器程式碼。語法圖裡面的矩形方框(term)變成了 term 方法,用於解析整數,expr 方法和語法圖的流程一致:
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
方法,加入直譯器的程式碼:
def term(self):
"""Return an INTEGER token value"""
token = self.current_token
self.eat(INTEGER)
return token.value
def expr(self):
"""Parser / Interpreter """
# 將輸入中的第一個標記設定成當前標記
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
方法被改成在合適的地方執行加法或減法操作,並傳回解釋的結果。儘管程式碼很直白,我建議花點時間去理解它。
進行下一步,看看完整的直譯器程式碼,好不?
這是新版計算器的原始碼,它可以處理包含有任意多個加法和減法運算的有效的數學運算式。
# 標記型別
#
# 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 is an index into self.text
self.pos = 0
# 當前標記實體
self.current_token = None
self.current_char = self.text[self.pos]
##########################################################
# Lexer code #
##########################################################
def error(self):
raise Exception('Invalid syntax')
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. One token at a time.
"""
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)
##########################################################
# Parser / Interpreter code #
##########################################################
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 term(self):
"""Return an INTEGER token value."""
token = self.current_token
self.eat(INTEGER)
return token.value
def expr(self):
"""Arithmetic expression parser / interpreter."""
# 將輸入中的第一個標記設定成當前標記
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:
# To run under Python3 replace 'raw_input' call
# 要在 Python3 下執行,請把 ‘raw_input’ 的呼叫換成 ‘input’
text = raw_input('calc> ')
except EOFError:
break
if not text:
continue
interpreter = Interpreter(text)
result = interpreter.expr()
print(result)
if __name__ == '__main__':
main()
把上面的程式碼儲存到 calc3.py
檔案中,或者直接從 GitHub[3] 上下載。試著執行它。看看它能不能處理我之前給你看過的語法圖裡面派生出的數學運算式。
這是我在自己的筆記本上執行的示例:
$ 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
記得我在文章開始時提過的練習嗎:它們在這兒,我保證過的:)
檢驗你的理解:
嘿,看!你看完了所有內容。感謝你們堅持到今天,而且沒有忘記練習。:) 下次我會帶著新的文章回來,盡請期待。
via: https://ruslanspivak.com/lsbasi-part3/
作者:Ruslan Spivak[5] 譯者:BriFuture 校對:wxy
本文由 LCTT 原創編譯,Linux中國 榮譽推出