Simplu calculator în Python - partea 3
Acest post face parte dintr-o serie în care eu fac un mic calculator în Python.
Data trecută am construit arborele sintactic corespunzător expresiei, iar acuma îl vom evalua. Aceasta este mult mai simplu decât parsarea, așa că hai să scăpăm. În final vom face apoi un REPL (read, evaluate, print loop), deci vom face o „consola” pentru calculatorul nostru. Aceasta este utilă pentru a putea beneficia de atribuirea de valori, că altfel ele se pierd după fiecare execuție.
Să începem cu testele:
from interpreter import interpreter
from tokenizer import Tokenizer
from tree import parseTree
pT = parseTree()
tok = Tokenizer()
interp = interpreter()
Tree = pT.buildParseTree(tok.tokenize("1+2"))
assert(interp.evaluate(Tree) == 3)
Tree = pT.buildParseTree(tok.tokenize("(5+(2\*3+2))-3\*((5+6)/2-4)"))
assert(interp.evaluate(Tree) == 8.5)
Tree = pT.buildParseTree(tok.tokenize("x = 2"))
assert(interp.evaluate(Tree) == 2)
Tree = pT.buildParseTree(tok.tokenize("y = 4^3"))
assert(interp.evaluate(Tree) == 64)
Tree = pT.buildParseTree(tok.tokenize("y^x\*2-3"))
assert(interp.evaluate(Tree) == 8189)
Tree = pT.buildParseTree(tok.tokenize("(x+(2\*y+2))-y\*((5+x)/2-4)"))
assert(interp.evaluate(Tree) == 164)
Tree = pT.buildParseTree(tok.tokenize("sin(10)"))
assert(interp.evaluate(Tree) == -0.5440211108893698)
Tree = pT.buildParseTree(tok.tokenize("2^(5+1)"))
assert(interp.evaluate(Tree) == 64)
Tree = pT.buildParseTree(tok.tokenize("(2+1)^(2+1)"))
assert(interp.evaluate(Tree) == 27)
from tokenizer import token,Tokenizer
from tree import tree,parseTree
import math
class interpreter:
functions = {
'sin':math.sin, 'cos':math.cos, 'ln':math.log, 'erfc':math.erfc, 'lgamma':math.lgamma,
'+':lambda a,b: a+b, '-':lambda a,b:a-b, '\*':lambda a,b:a*b, '/':lambda
a,b:a/b, '%':lambda a,b:a%b, '^':lambda a,b:a**b
}
def __init__(self):
self.vars = { }
În clasă avem definită o variabilă statică care conține toate funcțiile pe care le știe calculatorul nostru. Cele de pe prima linie sunt funcții matematice (cine știe ce sunt ultimele două și de ce nu este suficent math.erf
și math.gamma
?), iar pe a doua linie am făcut un mic hack pentru a evita scrierea a 7 if-uri pentru toate operațiile mai târziu. În constructor mai avem definit un dicționar care va conține variabilele noastre locale.
def evaluate(self,expression):
if isinstance(expression,tree):
if expression.value == '=':
return self.assign(expression)
left = self.getValue(expression.left)
right = self.getValue(expression.right)
if type(expression.value) == str and expression.value in self.functions:
if right!=None:
return self.functions[expression.value](left,right)
else:
return self.functions[expression.value](float(left))
raise InterpretError("Should be a tree here")
Aceasta este funcția principală a interpretorului. Expresia dată ar trebui să fie un arbore. Dacă avem atribuire, o facem într-o altă funcție, iar dacă nu calculăm valorile ramurilor și aplicăm funcția dată de valoarea nodului curent. Dacă ramura dreaptă este None
înseamnă că avem o funcție matematică, pentru că am stabilit că ele primesc un singur argument.
def assign(self,expression):
if not self.type(expression.left,'identifier'):
raise InterpretError("Assignments must be done to a variable!")
self.vars[expression.left.value] = self.getValue(expression.right)
return self.vars[expression.left.value]
def type(self,element,expected):
return element != None and type(element) != int and type(element) != float and type(element) != str and element.type == expected
La atribuire ne asigurăm mai întâi că în partea stângă avem doar o variabilă, iar apoi îi atribuim valoarea ramurii drepte, pe care o calculăm cu funcția de mai înainte. Funcția type este doar o funcție ajutătoare pentru a verifica tipul unui token/tree.
def getValue(self,element):
if self.type(element,'node'):
element = self.evaluate(element)
if self.type(element,'number'):
element = int(element.value)
if self.type(element,'identifier'):
if element.value in self.vars:
element = self.vars[element.value]
if type(element) == str:
if element in self.vars:
return self.vars[element]
else:
try:
element = int(element)
except ValueError:
raise InterpretError("Variable "+element+" doesn't exist")
return element
Această funcție returnează valoarea unei ramuri. Dacă este un arbore - îl evaluăm. Dacă este token de tip number, atunci returnăm întregul dat de valoarea lui. Dacă este un identificator - returnăm valoarea memorată. Dacă este de tip string, încercăm cele două ramuri iară. Această chestie este pentru că nu am fost consecvent la parsare și uneori returnam tokenul, iar altădată valoarea sa.
Și... cam atât. Interpretorul este cel mai scurt din cele 3 componente ale calculatorului nostru.
Shellul este la rândul lui destul de simplu. Instanțiem ce avem de instanțiat, afișăm câteva instrucțiuni elementare, iar apoi intrăm într-un ciclu infinit în care tot citim expresii de la tastatură și afișăm valorile lor. Dacă se scrie Exit, atunci îi mulțumim că s-a jucat cu noi și ieșim.
import interpreter
print("Enter your expressions to be calculated. Enter 'Exit' to exit the
calculator.")
expression = ""
tokenizer = interpreter.Tokenizer()
parser= interpreter.parseTree()
interpr = interpreter.interpreter()
while True:
expression = input('\> ')
if expression != "Exit":
try:
result = interpr.evaluate(parser.buildParseTree(tokenizer.tokenize(expression)))
print(result)
except Exception as e:
print(e)
else:
break
print("Thank you for using rolisz calculator!")
Dacă găsiți buguri vă rog să îmi spuneți. Tot codul sursă se găsește pe GitHub.