使用Python制作一個極簡四則運算解釋器
前言:
這是最近完成的一個小的 demo,一個極簡四則運算解釋器。前面,已經(jīng)基于這個想法發(fā)了兩篇博客了:
淺談一下四則運算和二叉樹
python的簡單四則運算語法樹可視化
然后,前兩天也就完成了這個總體的 demo 程序。本來整個程序的思路大致上有了,但是卡在了生成 AST 上了,因為不知道如何生成表達式樹的二叉樹,這個問題困擾了我很久。最后我參考了下面參考資料的第一篇博客的做法,不過它的樹的節(jié)點我感覺有點簡單了,而且最后底層葉子節(jié)點為 tree=tree(tree, "+", 0)
,感覺莫名其妙的,雖然這樣也是可以的。不過,我最終是定義了兩個節(jié)點來表示這個,一個 BinOp
來表示二元運算節(jié)點,另一個 Constant
來表示常量節(jié)點。正好元旦到了,我也能抽出來時間了,正好寫一篇博客記錄一下。
演示
這里來簡單放幾個程序的運行截圖,給大家看一下演示的效果。
計算功能演示
這里先展示了程序的幫助信息,然后是幾個簡單的四則運算測試,看起來是沒問題了(我可不敢保證,程序沒有bug!)。
輸出 tokens
輸出 AST
這個格式化的 JSON 信息太長了,不利于直接看到。我們將它渲染出來看最后生成的樹形圖(方法見前兩個博客)。保存下面這個 JSON 在一個文件中,這里我叫做 demo.json,然后執(zhí)行如下命令:pytm-cli -d LR -i demo.json -o demo.html
,然后再瀏覽器打開生成的 html 文件。
代碼
所有的代碼都在這里了,只需要一個文件 my_eval.py
,想要運行的話,復(fù)制、粘貼,然后按照演示的步驟執(zhí)行即可。
Node、BinOp、Constan 是用來表示節(jié)點的類.
Calculator 中 lexizer 方法是進行分詞的,本來我是打算使用正則的,如果你看過我前面的博客的話,可以發(fā)現(xiàn)我是用的正則來分詞的(因為 Python 的官方文檔正則表達式中有一個簡易的分詞程序)。不過我看其他人都是手寫的分詞,所以我也這樣做了,不過感覺并不是很好,很繁瑣,而且容易出錯。
parse 方法是進行解析的,主要是解析表達式的結(jié)構(gòu),判斷是否符合四則運算的文法,最終生成表達式樹(它的 AST)。
""" Grammar G -> E E -> T E' E' -> '+' T E' | '-' T E' | ? T -> F T' T' -> '*' F T' | '/' F T' | ? F -> '(' E ')' | num | name """ import json import argparse class Node: """ 簡單的抽象語法樹節(jié)點,定義一些需要使用到的具有層次結(jié)構(gòu)的節(jié)點 """ def eval(self) -> float: ... # 節(jié)點的計算方法 def visit(self): ... # 節(jié)點的訪問方法 class BinOp(Node): """ BinOp Node """ def __init__(self, left, op, right) -> None: self.left = left self.op = op self.right = right def eval(self) -> float: if self.op == "+": return self.left.eval() + self.right.eval() if self.op == "-": return self.left.eval() - self.right.eval() if self.op == "*": return self.left.eval() * self.right.eval() if self.op == "/": return self.left.eval() / self.right.eval() return 0 def visit(self): """ 遍歷樹的各個節(jié)點,并生成 JSON 表示 """ return { "name": "BinOp", "children": [ self.left.visit(), { "name": "OP", "children": [ { "name": self.op } ] }, self.right.visit() ] } class Constant(Node): """ Constant Node """ def __init__(self, value) -> None: self.value = value def eval(self) -> float: return self.value def visit(self): return { "name": "NUMBER", "children": [ { "name": str(self.value) # 轉(zhuǎn)成字符是因為渲染成圖像時,需要該字段為 str } ] } class Calculator: """ Simple Expression Parser """ def __init__(self, expr) -> None: self.expr = expr # 輸入的表達式 self.parse_end = False # 解析是否結(jié)束,默認未結(jié)束 self.toks = [] # 解析的 tokens self.index = 0 # 解析的下標 def lexizer(self): """ 分詞 """ index = 0 while index < len(self.expr): ch = self.expr[index] if ch in [" ", "\r", "\n"]: index += 1 continue if '0' <= ch <= '9': num_str = ch index += 1 while index < len(self.expr): n = self.expr[index] if '0' <= n <= '9': if ch == '0': raise Exception("Invalid number!") num_str = n index += 1 continue break self.toks.append({ "kind": "INT", "value": int(num_str) }) elif ch in ['+', '-', '*', '/', '(', ')']: self.toks.append({ "kind": ch, "value": ch }) index += 1 else: raise Exception("Unkonwn character!") def get_token(self): """ 獲取當(dāng)前位置的 token """ if 0 <= self.index < len(self.toks): tok = self.toks[self.index] return tok if self.index == len(self.toks): # token解析結(jié)束 return { "kind": "EOF", "value": "EOF" } raise Exception("Encounter Error, invalid index = ", self.index) def move_token(self): """ 下標向后移動一位 """ self.index += 1 def parse(self) -> Node: """ G -> E """ # 分詞 self.lexizer() # 解析 expr_tree = self.parse_expr() if self.parse_end: return expr_tree else: raise Exception("Invalid expression!") def parse_expr(self): """ E -> T E' E' -> + T E' | - T E' | ? """ # E -> E E' left = self.parse_term() # E' -> + T E' | - T E' | ? while True: tok = self.get_token() kind = tok["kind"] value = tok["value"] if tok["kind"] == "EOF": # 解析結(jié)束的標志 self.parse_end = True break if kind in ["+", "-"]: self.move_token() left = BinOp(left, value, self.parse_term()) else: break return left def parse_term(self): """ T -> F T' T' -> * F T' | / F T' | ? """ # T -> F T' left = self.parse_factor() # T' -> * F T' | / F T' | ? while True: tok = self.get_token() kind = tok["kind"] value = tok["value"] if kind in ["*", "/"]: self.move_token() right = self.parse_factor() left = BinOp(left, value, right) else: break return left def parse_factor(self): """ F -> '(' E ')' | num | name """ tok = self.get_token() kind = tok["kind"] value = tok["value"] if kind == '(': self.move_token() expr_node = self.parse_expr() if self.get_token()["kind"] != ")": raise Exception("Encounter Error, expected )!") self.move_token() return expr_node if kind == "INT": self.move_token() return Constant(value=value) raise Exception("Encounter Error, unknown factor: ", kind) if __name__ == "__main__": # 添加命令行參數(shù)解析器 cmd_parser = argparse.ArgumentParser( description="Simple Expression Interpreter!") group = cmd_parser.add_mutually_exclusive_group() group.add_argument("--tokens", help="print tokens", action="store_true") group.add_argument("--ast", help="print ast in JSON", action="store_true") cmd_parser.add_argument( "expr", help="expression, contains ['+', '-', '*', '/', '(', ')', 'num']") args = cmd_parser.parse_args() calculator = Calculator(expr=args.expr) tree = calculator.parse() if args.tokens: # 輸出 tokens for t in calculator.toks: print(f"{t['kind']:3s} ==> {t['value']}") elif args.ast: # 輸出 JSON 表示的 AST print(json.dumps(tree.visit(), indent=4)) else: # 計算結(jié)果 print(tree.eval())
總結(jié)
本來想在前面說一下為什么叫 my_eval.py
,但是感覺看到后面的人不多,那就在這里說好了。如果寫了一個復(fù)雜的表達式,那么怎么驗證是否正確的。這里我們直接利用 Python 這個最完美的解釋器就好了,哈哈。這里用 Python 的 eval 函數(shù),你當(dāng)然是不需要調(diào)用這個函數(shù),直接復(fù)制計算的表達式即可。我用 eval 函數(shù),只是想表達為什么我的程序會叫 my_eval
這個名字。
這樣實現(xiàn)下來,也算是完成了一個簡單的四則運算解釋器了。不過,如果你也做一遍的話,也估計會和我一樣感覺到整個過程很繁瑣。因為分詞和語法解析都有現(xiàn)成的工具可以來完成,而且不容易出錯,可以大大減少工作量。不過,自己來一遍也是很有必要的,在使用工具之前,至少也要了解工具的作用。
到此這篇關(guān)于使用Python制作一個極簡四則運算解釋器的文章就介紹到這了,更多相關(guān)Python極簡四則運算解釋器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python-sys.stdout作為默認函數(shù)參數(shù)的實現(xiàn)
今天小編就為大家分享一篇 python-sys.stdout作為默認函數(shù)參數(shù)的實現(xiàn),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-02-02Python模塊學(xué)習(xí)之subprocess詳解
subprocess是Python?2.4中新增的一個模塊,它允許你生成新的進程,連接到它們的?input/output/error?管道,并獲取它們的返回(狀態(tài))碼,下面小編就來和大家聊聊它的具體使用吧2023-08-08Python替換NumPy數(shù)組中大于某個值的所有元素實例
這篇文章主要介紹了Python替換NumPy數(shù)組中大于某個值的所有元素實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-06-06Win10下python 2.7與python 3.7雙環(huán)境安裝教程圖解
這篇文章主要介紹了Win10下python 2.7與python 3.7雙環(huán)境安裝教程,本文圖文并茂給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-10-10Python?數(shù)據(jù)可視化實現(xiàn)5種炫酷的動態(tài)圖
數(shù)據(jù)可以幫助我們描述這個世界、闡釋自己的想法和展示自己的成果,但如果只有單調(diào)乏味的文本和數(shù)字,我們卻往往能難抓住觀眾的眼球。而很多時候,一張漂亮的可視化圖表就足以勝過千言萬語2022-01-01python3實現(xiàn)TCP協(xié)議的簡單服務(wù)器和客戶端案例(分享)
下面小編就為大家?guī)硪黄猵ython3實現(xiàn)TCP協(xié)議的簡單服務(wù)器和客戶端案例(分享)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-06-062018年P(guān)ython值得關(guān)注的開源庫、工具和開發(fā)者(總結(jié)篇)
本文給大家總結(jié)了2018年P(guān)ython值得關(guān)注的開源庫、工具和開發(fā)者,需要的朋友可以參考下2018-01-01python 利用matplotlib在3D空間中繪制平面的案例
這篇文章主要介紹了python 利用matplotlib在3D空間中繪制平面的案例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02