使用Python制作一個(gè)極簡(jiǎn)四則運(yùn)算解釋器
前言:
這是最近完成的一個(gè)小的 demo,一個(gè)極簡(jiǎn)四則運(yùn)算解釋器。前面,已經(jīng)基于這個(gè)想法發(fā)了兩篇博客了:
淺談一下四則運(yùn)算和二叉樹(shù)
python的簡(jiǎn)單四則運(yùn)算語(yǔ)法樹(shù)可視化
然后,前兩天也就完成了這個(gè)總體的 demo 程序。本來(lái)整個(gè)程序的思路大致上有了,但是卡在了生成 AST 上了,因?yàn)椴恢廊绾紊杀磉_(dá)式樹(shù)的二叉樹(shù),這個(gè)問(wèn)題困擾了我很久。最后我參考了下面參考資料的第一篇博客的做法,不過(guò)它的樹(shù)的節(jié)點(diǎn)我感覺(jué)有點(diǎn)簡(jiǎn)單了,而且最后底層葉子節(jié)點(diǎn)為 tree=tree(tree, "+", 0),感覺(jué)莫名其妙的,雖然這樣也是可以的。不過(guò),我最終是定義了兩個(gè)節(jié)點(diǎn)來(lái)表示這個(gè),一個(gè) BinOp 來(lái)表示二元運(yùn)算節(jié)點(diǎn),另一個(gè) Constant 來(lái)表示常量節(jié)點(diǎn)。正好元旦到了,我也能抽出來(lái)時(shí)間了,正好寫一篇博客記錄一下。
演示
這里來(lái)簡(jiǎn)單放幾個(gè)程序的運(yùn)行截圖,給大家看一下演示的效果。
計(jì)算功能演示
這里先展示了程序的幫助信息,然后是幾個(gè)簡(jiǎn)單的四則運(yùn)算測(cè)試,看起來(lái)是沒(méi)問(wèn)題了(我可不敢保證,程序沒(méi)有bug?。?/p>

輸出 tokens

輸出 AST
這個(gè)格式化的 JSON 信息太長(zhǎng)了,不利于直接看到。我們將它渲染出來(lái)看最后生成的樹(shù)形圖(方法見(jiàn)前兩個(gè)博客)。保存下面這個(gè) JSON 在一個(gè)文件中,這里我叫做 demo.json,然后執(zhí)行如下命令:pytm-cli -d LR -i demo.json -o demo.html,然后再瀏覽器打開(kāi)生成的 html 文件。


代碼
所有的代碼都在這里了,只需要一個(gè)文件 my_eval.py,想要運(yùn)行的話,復(fù)制、粘貼,然后按照演示的步驟執(zhí)行即可。
Node、BinOp、Constan 是用來(lái)表示節(jié)點(diǎn)的類.
Calculator 中 lexizer 方法是進(jìn)行分詞的,本來(lái)我是打算使用正則的,如果你看過(guò)我前面的博客的話,可以發(fā)現(xiàn)我是用的正則來(lái)分詞的(因?yàn)?Python 的官方文檔正則表達(dá)式中有一個(gè)簡(jiǎn)易的分詞程序)。不過(guò)我看其他人都是手寫的分詞,所以我也這樣做了,不過(guò)感覺(jué)并不是很好,很繁瑣,而且容易出錯(cuò)。
parse 方法是進(jìn)行解析的,主要是解析表達(dá)式的結(jié)構(gòu),判斷是否符合四則運(yùn)算的文法,最終生成表達(dá)式樹(shù)(它的 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ǎn)單的抽象語(yǔ)法樹(shù)節(jié)點(diǎn),定義一些需要使用到的具有層次結(jié)構(gòu)的節(jié)點(diǎn)
"""
def eval(self) -> float: ... # 節(jié)點(diǎn)的計(jì)算方法
def visit(self): ... # 節(jié)點(diǎn)的訪問(wèn)方法
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):
"""
遍歷樹(shù)的各個(gè)節(jié)點(diǎn),并生成 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)成字符是因?yàn)殇秩境蓤D像時(shí),需要該字段為 str
}
]
}
class Calculator:
"""
Simple Expression Parser
"""
def __init__(self, expr) -> None:
self.expr = expr # 輸入的表達(dá)式
self.parse_end = False # 解析是否結(jié)束,默認(rèn)未結(jié)束
self.toks = [] # 解析的 tokens
self.index = 0 # 解析的下標(biāo)
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):
"""
下標(biāo)向后移動(dòng)一位
"""
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é)束的標(biāo)志
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: # 計(jì)算結(jié)果
print(tree.eval())
總結(jié)
本來(lái)想在前面說(shuō)一下為什么叫 my_eval.py,但是感覺(jué)看到后面的人不多,那就在這里說(shuō)好了。如果寫了一個(gè)復(fù)雜的表達(dá)式,那么怎么驗(yàn)證是否正確的。這里我們直接利用 Python 這個(gè)最完美的解釋器就好了,哈哈。這里用 Python 的 eval 函數(shù),你當(dāng)然是不需要調(diào)用這個(gè)函數(shù),直接復(fù)制計(jì)算的表達(dá)式即可。我用 eval 函數(shù),只是想表達(dá)為什么我的程序會(huì)叫 my_eval 這個(gè)名字。

這樣實(shí)現(xiàn)下來(lái),也算是完成了一個(gè)簡(jiǎn)單的四則運(yùn)算解釋器了。不過(guò),如果你也做一遍的話,也估計(jì)會(huì)和我一樣感覺(jué)到整個(gè)過(guò)程很繁瑣。因?yàn)榉衷~和語(yǔ)法解析都有現(xiàn)成的工具可以來(lái)完成,而且不容易出錯(cuò),可以大大減少工作量。不過(guò),自己來(lái)一遍也是很有必要的,在使用工具之前,至少也要了解工具的作用。
到此這篇關(guān)于使用Python制作一個(gè)極簡(jiǎn)四則運(yùn)算解釋器的文章就介紹到這了,更多相關(guān)Python極簡(jiǎn)四則運(yùn)算解釋器內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
python-sys.stdout作為默認(rèn)函數(shù)參數(shù)的實(shí)現(xiàn)
今天小編就為大家分享一篇 python-sys.stdout作為默認(rèn)函數(shù)參數(shù)的實(shí)現(xiàn),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-02-02
Python模塊學(xué)習(xí)之subprocess詳解
subprocess是Python?2.4中新增的一個(gè)模塊,它允許你生成新的進(jìn)程,連接到它們的?input/output/error?管道,并獲取它們的返回(狀態(tài))碼,下面小編就來(lái)和大家聊聊它的具體使用吧2023-08-08
Python替換NumPy數(shù)組中大于某個(gè)值的所有元素實(shí)例
這篇文章主要介紹了Python替換NumPy數(shù)組中大于某個(gè)值的所有元素實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-06-06
Win10下python 2.7與python 3.7雙環(huán)境安裝教程圖解
這篇文章主要介紹了Win10下python 2.7與python 3.7雙環(huán)境安裝教程,本文圖文并茂給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-10-10
Python?數(shù)據(jù)可視化實(shí)現(xiàn)5種炫酷的動(dòng)態(tài)圖
數(shù)據(jù)可以幫助我們描述這個(gè)世界、闡釋自己的想法和展示自己的成果,但如果只有單調(diào)乏味的文本和數(shù)字,我們卻往往能難抓住觀眾的眼球。而很多時(shí)候,一張漂亮的可視化圖表就足以勝過(guò)千言萬(wàn)語(yǔ)2022-01-01
python3實(shí)現(xiàn)TCP協(xié)議的簡(jiǎn)單服務(wù)器和客戶端案例(分享)
下面小編就為大家?guī)?lái)一篇python3實(shí)現(xiàn)TCP協(xié)議的簡(jiǎn)單服務(wù)器和客戶端案例(分享)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06
2018年P(guān)ython值得關(guān)注的開(kāi)源庫(kù)、工具和開(kāi)發(fā)者(總結(jié)篇)
本文給大家總結(jié)了2018年P(guān)ython值得關(guān)注的開(kāi)源庫(kù)、工具和開(kāi)發(fā)者,需要的朋友可以參考下2018-01-01
python 利用matplotlib在3D空間中繪制平面的案例
這篇文章主要介紹了python 利用matplotlib在3D空間中繪制平面的案例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-02-02

