簡介
在這篇文章中,我將向大家演示怎樣向一個通用計算器一樣解析並計算一個四則運算表示式。當我們結束的時候,我們將得到一個可以處理諸如 1+2*-(-3+2)/5.6+3樣式的表示式的計算器了。當然,你也可以將它擴充的更為強大。
我本意是想提供一個簡單有趣的課程來講解 語法分析 和 正規語法(編譯原理內容)。同時,介紹一下 PlyPlus,這是一個我斷斷續續改進了好幾年的語法解析 介面。作為這個課程的附加產物,我們最後會得到完全可替代eval()的一個安全的四則運算器。
如果你想在自家的電腦上試試本文中給的例子的話,你應該先安裝 PlyPlus ,使用命令pip install plyplus 。(譯者注:pip是一個包管理系統,用來安裝用python寫的軟體包,具體使用方法大家可以百度之或是google之,就不贅述了。)
本篇文章需要對python的繼承使用有所瞭解。
語法
對於那些不懂的如何解析和正式語法工作的人而言,這裡有一個快速的概覽:正式語法是用來解析文字的一些不同層面的規則。每一個規則都描述了相對應的那部分輸入的文字是如何組成的。
這裡是一個用來展示如何解析1+2+3+4的例子:
Rule #1 - add IS MADE OF add + number
OR number + number
或者用 EBNF:
add: add'+'number
| number'+'number
;
解析器每次都會尋找add+number或者number+number,找到一個之後就會將其轉換成add。基本上而言,每一個解析器的目標都在於儘可能的找到最高層次的表示式抽象。
以下是解析器的每個步驟:
number + number + number + number
第一次轉換將所有的Number變成“number”規則
[number + number] + number + number
解析器找到了它的第一個匹配模式!
[add + number] + number
在轉換成一個模式之後,它開始尋找下一個
[add + number]
add
這些有次序的符號變成了一個層次上的兩個簡單規則: number+number和add+number。這樣,只需要告訴計算機如果解決這兩個問題,它就能解析整個表示式。事實上,無論多長的加法序列,它都能解決! 這就是形式文法的力量。
運算子優先順序
算數表示式並不僅僅是符號的線性增長,運算子創造了一個隱式的層次結構,這非常適合用形式文法來表示:
1 + 2 * 3 / 4 - 5 + 6
這相當於:
1 + (2 * 3 / 4) - 5 + 6
我們可以透過巢狀規則表示此語法中的結構:
add: add+mul
| mul'+'mul
;
mul: mul '*; number
| number'*'number
;
透過將add設為操作mul而不是number,我們就得到了乘法優先的規則。
讓我們在腦海中模擬一下使用這個神奇的解析器來分析1+2*3*4的過程:
number + number * number * number
number + [number * number] * number
解析器不知道number+number的結果,所以這是它(解析器)的另一個選擇
number + [mul * number]
number + mul
現在我們遇到了一點困難! 解析器不知道如何處理number+mul。我們可以區分這種情況,但是如果我們繼續探索下去,就會發現有很多不同的沒有考慮到得可能,比如mul+number, add+number, add+add, 等等。
那麼我們應該怎麼做呢?
幸運的是,我們可以做一點小“把戲”:我們可以認為一個number本身是一個乘積,並且一個乘積本身是一個和!
這種思路一開始看起來有點古怪,不過它的確是有意義的:
add: add'+'mul
| mul'+'mul
| mul
;
mul: mul'*'number
| number'*'number
| number
;
但是如果 mul能夠變成 add, 且 number能夠變成 mul , 有些行的內容就變得多餘了。丟棄它們,我們就得到了:
add: add'+'mul
| mul
;
mul: mul'*'number
| number
;
讓我們來使用這種新的語法來模擬執行一下1+2*3*4:
number + number * number * number
現在沒有一個規則是對應number*number的了,但是解析器可以“變得有創造性”
number + [number] * number * number
number + [mul * number] * number
number + [mul * number]
[number] + mul
[mul] + mul
[add + mul]
add
成功了!!!
如果你覺得這個很奇妙,那麼嘗試著去用另一種算數表示式來模擬執行一下,然後看看錶達式是如何用正確的方式來一步步解決問題的。或者等著閱讀下一節中的內容,看看計算機是如何一步步執行出來的!
執行解析器
現在我們對於如何讓我們的語法運作起來已經有了非常不錯的想法了,那就寫一個實際的語法來應用一下吧:
start: add; // 這是最高層
add: add add_symbol mul | mul;
mul: mul mul_symbol number | number;
number:'[d.]+'; // 十進位制數的正規表示式
mul_symbol:'*'|'/';// Match * or /
add_symbol:'+'|'-';// Match + or -
你可能想要複習一下正規表示式,但不管怎樣,這個語法都非常直截了當。讓我們用一個表示式來測試一下吧:
>>>fromplyplusimportGrammar
>>> g=Grammar("""...""")
>>>printg.parse('1+2*3-5').pretty()
start
add
add
add
mul
number
1
add_symbol
+
mul
mul
number
2
mul_symbol
*
number
3
add_symbol
-
mul
number
5
幹得漂亮!
仔細研究一下這棵樹,看看解析器選擇了什麼層次。
如果你希望親自執行這個解析器,並使用你自己的表示式,你只需有Python即可。安裝Pip和PlyPlus之後,將上面的命令貼上到Python內(記得將'...'替換為實際的語法哦~)。
使樹成形
Plyplus會自動建立一棵樹,但它並不一定是最優的。將number放入到mul和將mul放入到add非常有利於建立一個階層,現在我們已經有了一個階層那它們反而會成為一個負擔。我們告訴Plyplus對它們加字首去“展開”(i.e.刪除)規則。
碰到一個@常常會展開一個規則,一個#則會壓平它,一個?會在它有一個子結點時展開。在這種情況下,?就是我們所需要的。
start: add;
?add: add add_symbol mul | mul; // Expand add if it's just a mul
?mul: mul mul_symbol number | number;// Expand mul if it's just a number
number:'[d.]+';
mul_symbol:'*'|'/';
add_symbol:'+'|'-';
在新語法下樹是這樣的:
>>> g=Grammar("""...""")
>>>printg.parse('1+2*3-5').pretty()
start
add
add
number
1
add_symbol
+
mul
number
2
mul_symbol
*
number
3
add_symbol
-
number
5
哦,這樣變得簡潔多了,我敢說,它是非常好的。
括號的處理及其它特性
目前為止,我們還明顯缺少一些必須的特性:括號,單元運算子(-(1+2)),及表示式中間允許存在空字元。其實這些特性都很容易就能實現,下面我們來嘗試一下。
需要先引入一個重要的概念:原子。在一個原子裡面(括號中及單元運算)發生的所有操作都優先於所有加法或乘法運算(包括位操作)。由於原子只是一個優先順序的構造器,並無語法意義,幫我們加上"@"符號以確保在編譯時它被能展開。
允許空格出現在表示式內最簡單的方法就是使用這種解釋方式:add SPACE add_symbol SPACE mul | mul; 但個解釋結果囉嗦且可讀性差。所有,我們需要令Plyplus總是忽略空格。
下面是完整的語法,包容了以上所述特性:
start: add;
?add: (add add_symbol)? mul;
?mul: (mul mul_symbol)? atom;
@atom: neg | number |'('add')';
neg:'-'atom;
number:'[d.]+';
mul_symbol:'*'|'/';
add_symbol:'+'|'-';
WHITESPACE:'[ t]+'(%ignore);
請確保理解這個語法再進入下一步:計算!
運算
現在,我們已經可以將一個表示式轉化成一棵分層樹了,只需要逐分支地掃描這棵樹,便可得到最終結果。
我們現在要開始編寫程式碼了,在此之前,我需要對這棵樹做兩點解釋:
1.每個分支都是包含如下兩個屬性的例項:
頭(head):規則的名字(例如add或者number);
尾(tail):包含所有與其匹配的子規則的列表。
2.Plyplus預設會刪除不必要的標記。在本例中,'( ' ,')' 和 '-' 會被刪除。但add和mul會有自己的規則,Plyplus會知道它們是必須的,從而不會被刪除它們。如果你需要保留這些標記,可以手動關掉這項功能,但從我的經驗來看,最好不要這樣做,而是手動修改相關語法效果更佳。
言歸正傳,現在我們開始編寫程式碼。我們將用一個非常簡單的轉換器來掃描這棵樹。它會從最外面的分支開始掃描,直到到達根節點為止,而我們的工作是告訴它如何掃描。如果一切順利的話,它將總會從最外層開始掃描!讓我們看看具體的實現吧。
>>>importoperator as op
>>>fromplyplusimportSTransformer
classCalc(STransformer):
def_bin_operator(self, exp):
arg1, operator_symbol, arg2=exp.tail
operator_func={'+': op.add,
'-': op.sub,
'*': op.mul,
'/': op.div }[operator_symbol]
returnoperator_func(arg1, arg2)
number =lambdaself, exp:float(exp.tail[0])
neg =lambdaself, exp:-exp.tail[0]
__default__=lambdaself, exp: exp.tail[0]
add=_bin_operator
mul=_bin_operator
每個方法都對應一個規則。如果方法不存在的話,將呼叫__default__方法。我們在其中省略了start,add_symbol和mul_symbol,因為它們只會返回自己的分支。
我使用了float()來解析數字,這是個懶方法,但我也可以用解析器來實現。
為了使語句整潔,我使用了運算子模組。例如add基本上是 'lambda x,y: x+y'之類的。
OK,現在我們執行這段程式碼來檢查一下結果。
>>> Calc().transform( g.parse('1 + 2 * -(-3+2) / 5.6 + 30'))
31.357142857142858
那麼eval()呢?7
>>>eval('1 + 2 * -(-3+2) / 5.6 + 30')
31.357142857142858
成功了:)
最後一步:REPL
為了美觀,我們把它封裝到一個不錯的計算器 REPL:
defmain():
calc=Calc()
whileTrue:
try:
s=raw_input('> ')
exceptEOFError:
break
ifs=='':
break
tree=calc_grammar.parse(s)
printcalc.transform(tree)
完整的程式碼可從這裡獲取:
https://github.com/erezsh/plyplus/blob/master/examples/calc.py