Simplu calculator în Python - partea 1
De mai multă vreme tot mă bate gândul să fac ceva interpretator pentru un limbaj mai simplu. Ca o primă aventurare în lumea interpretării șirurilor de caractere ca instrucțiuni pentru calculator, m-am decis să fac un simplu „calculator” în Python, care să aibă și suport pentru câteva funcții matematice și pentru variabile.
Basically, ce urmăresc eu e să am un mic progrămel care știe să facă următoarele:
7+10*sin(pi/2)/2-3^2 # să dea 3
x = 127-30*ln(e) # x să aibă valoarea 97
y = x^2 - 60/30*(6+x) # y să aibă 9203
Definiția riguroasă a sintaxei pe care o acceptă programul este următoarea:
<Number> = [0-9]+.?[0-9]* <Identifier> = [a-zA-Z][a-zA-Z0-9_]* <Operator> = [+*/%()^-]
<Function> = <Identifier> (<Identifier> | <Term>)
<Term> = <Number> | <Identfier> | LPAREN <Sum> RPAREN |
<Function>
<PowProduct> = <Term> (^ <Term>)?
<Product> = <PowProduct> ((*|/|%) <PowProduct>)*
<Sum> = <Product> ((+|-) <Product>)*
<Expression> = <Sum> | <Identifier> = <Expression>
Programul va avea 3 părți: un tokenizer, care va împărți stringul în bucăți elementare, un parser, care verifică corectitudinea datelor de intrare și apoi construiește un arbore de parsare, și în final un interpretator, care parcurge arborele și evaluează expresia.
În acest post mă voi ocupa de prima parte, de tokenizer.
Vom avea următoarele tipuri de tokene:
- numere întregi (10) sau cu zecimale (15.34)
- operatori (+,-,/,*,^,=,(,) )
- identificatori de variabile (x,y,rezultat) sau funcții (sin,ln,cos)
În primul rând avem nevoie de o clasă care să reprezinte tokenele, pe care o punem într-un fișier tokenizer.py
:
class token:
def __init__(self,type,value = None):
self.type = type
self.value = value
def __str__(self):
return self.type + " " + self.value
def __eq__(self, other):
if isinstance(other,token):
return self.type == other.type and self.value == other.value
elif isinstance(other,str):
return str(self.value) == other
else:
return False
Nu e prea complicată această clasă. Primește ca parametrii tipul și valoarea tokenului. Egalitatea între doi tokeni înseamnă că ambii au același tip și aceași valoare. Pentru a ne scurta un pic codul, permite și testarea cu un string, în acest caz având egalitate dacă valoare tokenului este egală cu stringul. Funcția __str__
o să o folosim doar la afișare.
Pentru că suntem dezvoltatori moderni și grijulii, vom aplica Test Driven Development, așa că mai întâi vom scrie câteva teste pentru cum ar trebui să funcționeze tokenizerul (la clasa token
nu avem ce să testăm). Testele le punem în alt fișier (ideal ar fi în alt modul, nu ne complicăm cu asta acuma), tokenizerTest.py
:
import tokenizer
t = tokenizer.Tokenizer()
assert (t.tokenize("1+2") == ['1',"+",'2'])
assert (t.tokenize("(x+y)\*2/4") ==
["(","x","+","y",")","\*","2","/","4"])
assert(t.tokenize("(x+(y\*z+2))-3\*((5+x)/2-4)") ==
["(","x","+","(","y","\*","z","+","2",")",")","-","3","\*","(","(","5","+","x",")","/","2","-","4",")"
])
assert(t.tokenize(" x + y ") == ['x','+','y'])
try:
t.tokenize("_&amp;amp;x+y") \# test for invalid symbols
assert(False)
except tokenizer.TokenizerError:
assert(True)
Tokenizerul va parcurge pe rând fiecare caracter din șirul de caractere și la fiecare pas va încerca să găsească ce tip de token se potrivește. Dacă la un punct nu găsește un token potrivit, înseamnă că șirul dat nu este o expresie validă. Așa că în fișierul tokenizer.py
adăugăm clasa Tokenizer. Folosim o clasă doar ca să încapsulăm funcțiile să nu poluăm namespaceul general:
class TokenizerError(Exception): # exceptie speciala pentru cazul in care nu se recunoaste un caracter
pass
class Tokenizer:
def tokenize(self,string):
self.string = string
self.tokens = []
self.index = 0
match = False # am gasit un token iteratia asta
while self.index < len(self.string):
token = self.operator()
if token:
self.tokens.append(token)
match = True
token = self.identifier()
if token:
self.tokens.append(token)
match = True
token = self.number()
if token:
self.tokens.append(token)
match = True
if self.skipWhite(): # sarim peste spatiile libere
match = True
if not match: # daca nu am gasit token iteratia aceasta inseamna ca
# avem simboluri incorecte
raise TokenizerError('Invalid simbol found:'+self.string[self.index])
match = False
return self.tokens
Ca să detectăm un operator pur și simplu comparăm caracterul cu operatorii posibili:
def operator(self):
if self.index < len(self.string) and self.string[self.index] in '+-/\*()%^=':
t = token('operator',self.string[self.index])
self.index +=1
return t
else:
return None
Detectare unui identificator e un pic mai complicată, deoarece poate avea lungime variabilă și poate fi formată din tot felul de caractere: începe doar cu litere, dar apoi pot fi și cifre sau liniuțe de subliniere, în afara literelor. Vom folosi un regex pentru aceasta (acuma nu o să explic ce este un regex, doar pe scurt ce face fiecare componentă: ^[a-zA-Z][a-zA-Z0-9_]*$
. ^ face ca potrivirea să se facă la începutul stringului, [a-zA-Z] corespunde unei litere, [a-zA-Z0-9_] corespunde unei litere, cifre sau unui underscore, iar * face repetitivă și opțională ultima chestie. Identificatorul, dacă există, va fi dat de match.group()
:
def identifier(self):
pattern = '^[a-zA-Z][a-zA-Z0-9_]\*'
match = re.match(pattern,self.string[self.index:])
if match:
t = token('identifier',match.group())
self.index = self.index + match.end()
return t
return None
Pentru a identifica un număr, facem aceași chestie, doar că schimbăm regexul: [0-9]+.?[0-9]
. + face ca termenul anterior să se repete, dar să fie cel puțin o dată, . face "escaping" la ., care este caracter special în regexuri, iar ? face ca termenul anterior să fie opțional.
def number(self):
pattern = "^[0-9]+\\.?[0-9]\*"
match = re.match(pattern,self.string[self.index:])
if match:
t = token('number',match.group())
self.index = self.index + match.end()
return t
return None
Funcția skipWhite face un singur lucru: sare peste spații și returnează True dacă a sărit peste ceva.
def skipWhite(self):
match = False
while self.index < len(self.string) and self.string[self.index] == " ":
self.index+=1
match = True
return match
Punând toate acestea împreună, avem un simplu tokenizer pentru expresii matematice. Stay tuned for part 2, the parser.