# 練習 34:分析器
> 原文:[Exercise 34: Analyzers](https://learncodethehardway.org/more-python-book/ex34.html)
> 譯者:[飛龍](https://github.com/wizardforcel)
> 協議:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 自豪地采用[谷歌翻譯](https://translate.google.cn/)
你現在有了一個解析器,它應該生成一個語法產生式對象樹。我會將其稱為“解析樹”,這意味著你可以從“解析樹的頂部開始,然后“遍歷”它,直到你訪問每個節點來分析整個程序。當你了解`BSTree`和`TSTree`數據結構時,你已經做了這樣的事情。你從頂部開始訪問了每個節點,并且你訪問的順序(深度優先,廣度優先,順序遍歷等)確定了節點的處理方式。你的解析樹具有相同的功能,編寫微型 Python 解釋器的下一步是遍歷樹并分析它。
分析器的任務是,在你的語法中找到語義錯誤,并修復或添加下一階段需要的信息。語義錯誤是錯誤,雖然語法正確,但并不適合 Python 程序。這可以是一個尚未定義的遍歷,也可以是不符合邏輯的代碼,它根本沒有意義。一些語言語法是如此松散,分析器必須做更多的工作來修復解析樹。其他語言很容易解析和處理,甚至不需要分析器的步驟。
為了編寫分析器,你需要一種方法來訪問解析樹中的每個節點,分析錯誤,并修復任何缺少的信息。有三種通用方法可以用于實現它:
+ 你創建一個分析器,它知道如何更新每個語法產生式。它將以和解析器相似的方式遍歷解析樹,對每種生產式類型都擁有一個函數,但他的任務是更改,更新和檢查產生式。
+ 你改變你的語法產生式,讓他們知道如何分析自己的狀態。那么你的分析器就僅僅是一個引擎,它遍歷解析樹,調用每個產生式的`analyze()`方法。使用這種風格,你將需要一些狀態,它們會傳遞給每個語法產生式類,這個類應該是第三個類。
+ 你創建一組單獨的類來實現最終分析后的樹,你可以將其傳遞給解釋器。通過許多方式,你將使用一組新的類來映射語法分析器的語法產生式,這些新的類接受全局狀態,語法產生式,并配置其`__init__`,使其為分析后的結果。
我建議你現在使用 #2 或 #3 來完成挑戰練習。
## 訪客模式
“訪問者模式”是面向對象語言中非常常見的技術,其中你可以創建一些類,它們知道被“訪問”時應該做什么。這可以讓你將處理某個類的代碼集成到這個類。這樣做的優點是,你不需要大型的`if`語句來檢查類上的類型,來了解該做什么。相反,你只需創建一個類似于這個的類:
```py
class Foo(object):
def visit(self, data):
# do stuff to self for foo
```
一旦你擁有了這個類(`visit `可以叫任何東西),你可以遍歷列表來調用它。
```py
for action in list_of_actions:
action.visit(data)
```
你將使用這種模式用于 #2 或 #3 風格的分析器;唯一的區別是:
+ 如果你決定,你的語法產生式也將是分析結果,那么你的`analyze()`函數(也就是我們的`visit()`)只會將該數據存儲在產生式類,或者在提供給它的狀態中。
+ 如果你決定,你的語法產生式將為解釋器生成另一組類(請參閱練習 35),那么每次`analyze`的調用都將返回一個新對象,該對象將放入列表中以供以后使用,或將其作為子樹附加到當前對象。
我將介紹第一種情況,其中你的語法產生式也是你的分析器結果。這適用于我們簡單的微型 Python 腳本,你應該遵循這種風格。如果你想嘗試其他的設計,那么你可以之后嘗試。
## 簡短的微型 Python 分析器
> 警告
> 如果你想自己嘗試,為你的語法產生式嘗試實現訪客模式,那么你應該停在這里。我將給出一個相當完整但簡單的例子,它充滿了障礙。
訪客模式背后的概念似乎是奇怪的,但它是完全有意義的。每個語法產生式都知道在不同階段應該做什么,所以你可以把這個階段代碼放在需要的數據附近。為了演示這個,我寫了一個小型的偽造的`PunyPyAnalyzer`,它僅僅使用訪客模式打印出解析。我只完成一個語法產生式的樣例,所以你可以理解這是如何完成的。我不想給你太多的線索。
我做的第一件事是,定義一個`Production`類,我的所有語法產生式都將繼承它。
```py
class Production(object):
def analyze(self, world):
"""Implement your analyzer here."""
```
它擁有我的初始的`analyze()`方法,并接受我們隨后使用的`PunyPyWorld`。第一個語法產生式的示例使`FuncCall`產生式:
```py
class FuncCall(Production):
def __init__(self, name, params):
self.name = name
self.params = params
def analyze(self, world):
print("> FuncCall: ", self.name)
self.params.analyze(world)
```
函數調用有名稱和`params`,它是一個`Parameters`產生式類,用于函數調用的參數。看看`analyze()`方法,你會看到第一個訪客函數。當你訪問`PunyPyAnalyzer`時,你將看到如何運行,但是請注意,此函數之后在每個函數的參數上調用`param.analyze(world)`:
```py
class Parameters(Production):
def __init__(self, expressions):
self.expressions = expressions
def analyze(self, world):
print(">> Parameters: ")
for expr in self.expressions:
expr.analyze(world)
```
這就產生了`Parameters`類,它包含每個表達式,它們組成函數的參數。`Parameters.analyze`僅僅遍歷它的表達式列表,其中我們擁有兩個:
```py
class Expr(Production): pass
class IntExpr(Expr):
def __init__(self, integer):
self.integer = integer
def analyze(self, world):
print(">>>> IntExpr: ", self.integer)
class AddExpr(Expr):
def __init__(self, left, right):
self.left = left
self.right = right
def analyze(self, world):
print(">>> AddExpr: ")
self.left.analyze(world)
self.right.analyze(world)
```
在這個例子中,我只添加了兩個數字,但是我創建一個基本的`Expr`類,然后創建`IntExpr`和`AddExpr`類。每個都僅僅擁有`analyze()`方法,打印出其內容。
因此,我們有用于分析樹的類,我們可以做一些分析。我們需要的第一件事是一個世界,它可以跟蹤變量定義、函數、以及我們的`Production.analyze()`方法所需的其他東西。
```py
class PunyPyWorld(object):
def __init__(self, variables):
self.variables = variables
self.functions = {}
```
當調用任何`Production.analyze()`方法時,`PunyPyWorld`對象被傳遞給它,因此`analyze()`方法知道世界的狀態。它可以更新變量,尋找函數,并在世界中執行任何所需的事情。
然后我們需要一個`PunyPyAnalyzer`,它可以接受解析樹和世界,并使所有的語法產生式運行:
```py
class PunyPyAnalyzer(object):
def __init__(self, parse_tree, world):
self.parse_tree = parse_tree
self.world = world
def analyze(self):
for node in self.parse_tree:
node.analyze(self.world)
```
函數的簡單調用`hello(10 + 20)`的配置相當簡單。
```py
variables = {}
world = PunyPyWorld(variables)
# simulate hello(10 + 20)
script = [FuncCall("hello",
Parameters(
[AddExpr(IntExpr(10), IntExpr(20))])
)]
analyzer = PunyPyAnalyzer(script, world)
analyzer.analyze()
```
要確保你理解了我構造`script`的方式。注意到第一個參數是一個列表了嘛?
## 解析器與分析器
在這個例子中,我假設`PunyPyParser`已將`NUMBER`記號轉換為整數。在其他語言中,你可能只擁有記號,并讓`PunyPyAnalyzer`進行轉換。這一切都取決于,你想讓錯誤發生在哪里,以及哪里可以做最有用的分析。如果你將工作放在解析器中,那么你可以馬上給出格式化方面的早期錯誤。如果將其放在分析器中,那么你可以給出錯誤,使用整個解析文件來有所幫助。
## 挑戰練習
所有這些`analyze()`方法的要點不僅僅是打印出來,而是改變每個`Production`子類的內部狀態,以便解釋器可以像腳本一樣運行它。你在這個練習中的任務是,接受你的語法產生式類(可能與我的不同)并進行分析。
隨意借鑒我的出發點。如果需要,可以使用我的分析器和我的世界,但是你應該嘗試首先編寫自己的分析器。你還應該將練習 33 中的產生式類與我的比較。你的更好嗎?它們能支持這種設計嗎?如果他們不能則改變它們。
你的分析器需要做一些事情才能使解釋器正常工作:
+ 跟蹤變量定義。在一個實際的語言中,這將需要一些非常復雜的嵌套表,但是對于微型 Python 來說,只需假設有一個巨大的表(`TSTree`或`dict`),所有變量都在這里。這意味著`hello(x, y)`函數的`x`和`y`參數實際上是全局變量。
+ 跟蹤函數的位置,以便以后運行它們。我們的微型 Python 只有簡單的函數,但是當`Interpreter`運行時,它需要“跳轉”到并運行它們。最好的辦法保留它們,便于之后使用。
+ 檢查你可以想到的任何錯誤,例如使用中缺少的變量。這是棘手的,因為 Python 這樣的語言,在解釋器階段中進行更多的錯誤檢查。你應該決定在分析過程中,可能出現哪些錯誤并實現它們。例如,如果我嘗試使用未定義的變量,會發生什么?
+ 如果你正確地實現了 Python `INDENT`語法,那么你的`FuncCall`產生式應該有額外的代碼。解釋器將需要它來運行它,所以確保有一個實現它的方式。
## 研究性學習
+ 這個練習已經很難了,但是如何創建一個更好的方式,來存儲變量,至少實現一個額外的作用域層級?記得“作用域”的概念是,`hello(x, y)`中的`x, y`不影響`hello`函數之外的你定義`x`和`y`。
+ 在`Scanner`,`Parser`和`Analyzer`中實現賦值。這意味著我應該可以執行`x = 10 + 14`,你可以處理它。
## 深入學習
研究“基于表達式”和“基于語句”的編程語言之間的區別。較短版本是一些只有表達式的語言,所以任何東西都有與之相關的某種(返回)值。其他語言的表達式擁有值,語句沒有,因此把它們賦給變量會失敗。Python 是哪種語言?
- 笨辦法學 Python · 續 中文版
- 引言
- 第一部分:預備知識
- 練習 0:起步
- 練習 1:流程
- 練習 2:創造力
- 練習 3:質量
- 第二部分:簡單的黑魔法
- 練習 4:處理命令行參數
- 練習 5:cat
- 練習 6:find
- 練習 7:grep
- 練習 8:cut
- 練習 9:sed
- 練習 10:sort
- 練習 11:uniq
- 練習 12:復習
- 第三部分:數據結構
- 練習 13:單鏈表
- 練習 14:雙鏈表
- 練習 15:棧和隊列
- 練習 16:冒泡、快速和歸并排序
- 練習 17:字典
- 練習 18:性能測量
- 練習 19:改善性能
- 練習 20:二叉搜索樹
- 練習 21:二分搜索
- 練習 22:后綴數組
- 練習 23:三叉搜索樹
- 練習 24:URL 快速路由
- 第四部分:進階項目
- 練習 25:xargs
- 練習 26:hexdump
- 練習 27:tr
- 練習 28:sh
- 練習 29:diff和patch
- 第五部分:文本解析
- 練習 30:有限狀態機
- 練習 31:正則表達式
- 練習 32:掃描器
- 練習 33:解析器
- 練習 34:分析器
- 練習 35:解釋器
- 練習 36:簡單的計算器
- 練習 37:小型 BASIC
- 第六部分:SQL 和對象關系映射
- 練習 38:SQL 簡介
- 練習 39:SQL 創建
- 練習 40:SQL 讀取
- 練習 41:SQL 更新
- 練習 42:SQL 刪除
- 練習 43:SQL 管理
- 練習 44:使用 Python 的數據庫 API
- 練習 45:創建 ORM
- 第七部分:大作業
- 練習 46:blog
- 練習 47:bc
- 練習 48:ed
- 練習 49:sed
- 練習 50:vi
- 練習 51:lessweb
- 練習 52:moreweb