一起來寫一個簡單的解釋器(1)

一起來寫一個簡單的解釋器(1)

1 人贊了文章

在我的《編譯器與解釋器的區別和工作原理》一文中已經對編譯器和解釋器進行了講述,在開始這個系列的學習之前,建議新手朋友先了解一下這篇文章。

本系列文章參考國外編程高手魯斯蘭的博客文章《Let』s Build A Simple Interpreter》。

從網上看到了這個系列的文章,感覺很棒,於是把文章的精華整理出來和大家分享。

我並不打算直接翻譯原文,而是通過對原文的理解,用自己的方式來闡述文章中的主要內容。

那麼,為什麼要學習編譯器和解釋器呢?

按原文中的話來說,編寫一個解釋器需要綜合很多編程技能,並且有效的提高這些技能;而且,還能夠幫助我們了解編譯器和解釋器是如何工作的以及計算機是怎麼工作的。

接下來,我們就開始一起來學習如何編寫一個簡單的解釋器。

實際上,我們使用的計算器的程序就是一個解釋器,具體來說是一個四則運算表達式的解釋器。

所以,我們可以通過編寫一個計算器程序,來初步了解編譯器和解釋器。

在本文中,我們先來完成加法的運算。

因為我們編寫的程序功能要逐步完善,為了保證本文中編寫的程序不出錯,我們在輸入時要注意以下幾點:

  • 只能輸入個位整數;
  • 只能進行加法;
  • 輸入的表達式中不能有空格;
  • 只能輸入一次加法運算的表達式,例如:6+9。

一、定義常量

哪些內容是常量呢?

常量包括表達式中的數字類型、運算符以及結束標識。

示例代碼:

INTEGER, PLUS, EOF = INTEGER, PLUS, EOF # 整數,加法,結束標識

二、定義類

都需要定義哪些類呢?

這裡,我們需要分析計算器工作的過程。

在用戶輸入了一個四則運算表達式之後,我們需要讓程序讀懂這個表達式,然後進行計算。

表達式的結構是:[數字][運算符][數字]

我們首先要做的應該是將一段表達式中的每個部分取出,然後進行運算處理。

那麼,表達式中取出的每一部分,我們都看做是一個記號(Token)。

每個記號都是一個對象,這個對象需要通過類來實例化。

1、記號類(Token)

示例代碼:

class Token: # 定義記號類 def __init__(self, value_type, value): # 定義構造方法 self.value_type = value_type # 記號中值的類型 self.value = value # 記號中的值 def __str__(self): # 重寫查看記號內容的方法 return Token({char_type},{value}).format(char_type=self.value_type, value=self.value) def __repr__(self): # 也可以寫成 __repr__=__str__ return self.__str__()

在Token類中,除了初始化的構造方法,還有「__str__()」和「__repr__()」這兩個魔法方法。

這兩個方法都可以用來返回一個可以表示對象的字元串。

區別在於,「__str__()」方法是顯示給用戶的,「__repr__()」方法是顯示給程序開發人員看的。

為了能夠理解這個區別,大家可以嘗試在Python的交互環境中輸入以下代碼。

示例代碼:

>>>class A:... def __str__(self):... return str方法的返回值... def __repr__(self):... return repr方法的返回值>>>a=A()>>>arepr方法的返回值>>>print(a)str方法的返回值>>>str(a)str方法的返回值>>>repr(a)repr方法的返回值

大家能夠看到,當我們將類實例化,直接輸入對象名稱回車後,此時調用的是「__repr__()」方法。

而「print()」方法是給用戶友好的顯示,通過「print()」方法列印對象,則調用的是「__str__()」方法。

當然,如果我們沒有重寫「__str__()」方法的話,通過「print()」方法列印對象,則調用的是「__repr__()」方法。

另外,大家還能看到,當使用內置函數「str()」時,調用的是「__str__()」方法。

而使用內置函數「repr()」時,調用的是「__repr__()」方法。

所以,關於這兩個方法的重寫,當我們想在所有環境中都統一顯示內容的話,可以只重寫「__repr__()」方法;當我們想在不同環境中有不同顯示內容的話(例如:用戶和開發人員看到不同的內容),可以分別重寫「__str__()」和「__repr__()」方法,實際上「__str__()」方法只是覆蓋了「__repr__」方法,以得到更友好的顯示內容,呈現給用戶。

好了,讓我們回歸解釋器的編寫。

2、解釋器類(Interpreter)

解釋器就像一個工具,具備各種功能,通過它的各種功能實現解釋過程。

一個工具是一個對象,他也應該由類的實例化產生。

那麼,一個解釋器都要包含哪些功能呢?

具體如下:

  • 輸入錯誤時拋出異常;
  • 獲取每一個記號;
  • 驗證記號流是否符合運算結構,當通過驗證,給出計算結果。

示例代碼:

class Interpreter: # 定義解釋器類 def __init__(self, text): # 定義構造方法獲取用戶輸入的表達式 self.text = text # 用戶輸入的表達式 self.position = 0 # 獲取表達式中每一個字元時的位置 self.current_token = None # 臨時保存記號的變數 def error(self): # 定義提示錯誤的方法 raise Exception(警告:錯誤的輸入內容!) # 拋出異常 def get_next_token(self): # 定義獲取記號的方法 text = self.text if self.position >= len(text): # 如果獲取字元的位置已經到達末端 return Token(EOF, None) # 返回結束標識的記號對象 current_char = text[self.position] # 獲取當前位置的字元 if current_char.isdigit(): # 如果當前位置的字元是數字 token = Token(INTEGER, int(current_char)) # 實例化整數的記號對象 self.position += 1 # 獲取字元的位置自增1,以便獲取下一個字元。 return token # 返回記號對象 if current_char == +: # 如果當前位置的字元是加號 token = Token(PLUS, current_char) # 實例化加號運算符的記號對象 self.position += 1 # 獲取字元的位置自增1,以便獲取下一個字元。 return token # 返回記號對象 self.error() # 如果以上沒有任何對象返回,拋出異常。 def expr(self): # 定義驗證運算結構並計算結果的方法 self.current_token = self.get_next_token() # 獲取第一個記號 left = self.current_token # 保存第1個記號到變數 if self.current_token.value_type == INTEGER: # 如果記號中的值類型是整數 self.current_token = self.get_next_token() # 獲取下一個記號對象存入變數 else: # 否則 self.error() # 拋出異常 operator = self.current_token # 保存第2個記號到變數 if self.current_token.value_type == PLUS: # 如果記號中的值類型是加號 self.current_token = self.get_next_token() # 獲取下一個記號對象存入變數 else: # 否則 self.error() # 拋出異常 right = self.current_token # 保存第3個記號到變數 if self.current_token.value_type == INTEGER: # 如果記號中的值類型是整數 self.current_token = self.get_next_token() # 獲取下一個記號對象存入變數 else: # 否則 self.error() # 拋出異常 result = left.value + right.value # 進行加法運算獲取結果 return result # 返回計算結果

在上方代碼中,「expr()」方法是負責運算的方法。

在這個方法中,我們能夠看到,它在一個一個的獲取表達式中的字元,並進行驗證。

如果「left」、「operator」和「right」這3個變數中保存的記號,符合「[數字][運算符][數字]」的運算結構,就進行加法運算,返回結果。

不過在「expr()」方法中,大家能看到很多重複的相類似的語句,也就是標紅部分的代碼。

這樣的代碼,我們可以進行抽象處理,把它們獨立為一個單獨的方法。

這部分代碼的功能,主要是驗證當前的記號是不是符合運算要求的類型,如果符合要求就「吃掉」當前的記號,獲取下一個記號保存到臨時保存記號的變數中。

經過抽象後,新的代碼如下:

def eat(self, token_type): # 定義輔助運算的方法,此方法用於驗證記號對象的值類型是否符合運算要求。 if self.current_token.value_type == token_type: # 如果記號中的值類型符合運算要求 self.current_token = self.get_next_token() # 獲取下一個記號對象存入變數 else: # 否則 self.error() # 拋出異常def expr(self): # 定義驗證運算結構並計算結果的方法 self.current_token = self.get_next_token() # 獲取第一個記號 left = self.current_token # 保存第1個記號到變數 self.eat(INTEGER) # 調用驗證方法傳入運算要求的類型 operator = self.current_token # 保存第2個記號到變數 self.eat(PLUS) # 調用驗證方法傳入運算要求的類型 right = self.current_token # 保存第3個記號到變數 self.eat(INTEGER) # 調用驗證方法傳入運算要求的類型 result = left.value + right.value # 進行加法運算獲取結果 return result # 返回計算結果

三、定義主函數並執行

示例代碼:

def main(): while True: # 循環獲取輸入 try: text = input(>>>) # 獲取用戶輸入 except EOFError: # 捕獲到末端錯誤時退出 break if not text: # 如果未輸入時繼續提示輸入 continue interpreter = Interpreter(text) # 實例化解釋器對象 result = interpreter.expr() # 執行運算方法獲取運算結果 print(text, =, result) # if __name__ == __main__: main()

到這裡我們就完成了一個初步的能夠解釋加法表達式的解釋器。

最後,我們需要掌握一些概念。

將輸入的字元串表達式切分為記號的過程叫做詞法分析

解釋器工作時,第一步就需要讀取輸入內容,並能夠將內容轉換為一系列的記號。完成這部分工作的組件叫做詞法分析器(Lexer:Lexical Ananlyzer的簡稱),也可以叫做掃描器(Scanner)或者分詞器(Tokenizer)。

以上就是《一起來寫一個簡單的解釋器》系列文章的第一篇內容。

在這篇內容的基礎上,大家可以嘗試進行以下功能的擴展:

  • 讓解釋器支持減法運算;
  • 讓解釋器支持多位整數的運算,例如:12+36;
  • 添加一個方法,讓解釋器能夠處理用戶所輸入表達式中的空格。

而且,當學習完這篇文章,請大家自我檢查一下,是否了解了以下內容:

  • 什麼是解釋器?
  • 什麼是編譯器?
  • 解釋器和編譯器的區別是什麼?
  • 什麼是記號(Token)?
  • 將輸入內容切分為記號的過程叫什麼?
  • 解釋器工作時,負責詞法分析的組件叫什麼?
  • 詞法分析的組件還有哪些其它的常用名稱?

需要項目源代碼下載的朋友,可以留言關注,或者留下你的郵箱喔!


推薦閱讀:

XXX 語言是世界上最好語言
C語言基礎:指針做參數
偽·從零開始學Python - 1.1 認識Python
解讀C 語言指針
PAT團體程序設計天梯賽-練習集答案

TAG:編程語言 | Python | 科技 |