rolisz's site

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 core­spun­ză­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 cal­cu­la­torul 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 cal­cu­la­torul 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 con­struc­tor mai avem definit un dicționar care va conține vari­abilele 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 in­ter­pre­toru­lui. 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 iden­ti­fi­ca­tor - 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. In­ter­pre­torul este cel mai scurt din cele 3 componente ale cal­cu­la­toru­lui nostru.

Shellul este la rândul lui destul de simplu. Instanțiem ce avem de instanțiat, afișăm câteva in­strucți­u­ni 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.