Bir Interpreter Yazalım | Part 2
Eğer sadece yöntemleri öğrenirseniz, onlara bağlı olursunuz. Ama ilkeleri öğrenirseniz, kendi yöntemlerinizi tasarlayabilirsiniz.
Bugün, Part 1’de yazdığımız hesap makinesini geliştireceğiz, aşağıdakileri yapabiliyor olacak.
- Karakter dizisinin herhangi bir yerindeki boşlukları ele alabilme.
- Çok haneli sayıları değerlendirebilme.
- İki adet tam sayıyı arasında çıkarma işlemi yapabilme.
Yukarıda bahsettiğimiz tüm özelliklere sahip hesap makinemizin yeni versiyonu aşağıda,
# Token types
# EOF (end-of-file) token is used to indicate that
# there is no more input left for lexical analysis
INTEGER, PLUS, MINUS, EOF = 'INTEGER', 'PLUS', 'MINUS', 'EOF'
class Token(object):
def __init__(self, type, value):
# token type: INTEGER, PLUS, MINUS, or EOF
self.type = type
# token value: non-negative integer value, '+', '-', or None
self.value = value
def __str__(self):
"""String representation of the class instance.
Examples:
Token(INTEGER, 3)
Token(PLUS '+')
"""
return 'Token({type}, {value})'.format(
type=self.type,
value=repr(self.value)
)
def __repr__(self):
return self.__str__()
class Interpreter(object):
def __init__(self, text):
# client string input, e.g. "3 + 5", "12 - 5", etc
self.text = text
# self.pos is an index into self.text
self.pos = 0
# current token instance
self.current_token = None
self.current_char = self.text[self.pos]
def error(self):
raise Exception('Error parsing input')
def advance(self):
"""Advance the 'pos' pointer and set the 'current_char' variable."""
self.pos += 1
if self.pos > len(self.text) - 1:
self.current_char = None # Indicates end of input
else:
self.current_char = self.text[self.pos]
def skip_whitespace(self):
while self.current_char is not None and self.current_char.isspace():
self.advance()
def integer(self):
"""Return a (multidigit) integer consumed from the input."""
result = ''
while self.current_char is not None and self.current_char.isdigit():
result += self.current_char
self.advance()
return int(result)
def get_next_token(self):
"""Lexical analyzer (also known as scanner or tokenizer)
This method is responsible for breaking a sentence
apart into tokens.
"""
while self.current_char is not None:
if self.current_char.isspace():
self.skip_whitespace()
continue
if self.current_char.isdigit():
return Token(INTEGER, self.integer())
if self.current_char == '+':
self.advance()
return Token(PLUS, '+')
if self.current_char == '-':
self.advance()
return Token(MINUS, '-')
self.error()
return Token(EOF, None)
def eat(self, token_type):
# compare the current token type with the passed token
# type and if they match then "eat" the current token
# and assign the next token to the self.current_token,
# otherwise raise an exception.
if self.current_token.type == token_type:
self.current_token = self.get_next_token()
else:
self.error()
def expr(self):
"""Parser / Interpreter
expr -> INTEGER PLUS INTEGER
expr -> INTEGER MINUS INTEGER
"""
# set current token to the first token taken from the input
self.current_token = self.get_next_token()
# we expect the current token to be an integer
left = self.current_token
self.eat(INTEGER)
# we expect the current token to be either a '+' or '-'
op = self.current_token
if op.type == PLUS:
self.eat(PLUS)
else:
self.eat(MINUS)
# we expect the current token to be an integer
right = self.current_token
self.eat(INTEGER)
# after the above call the self.current_token is set to
# EOF token
# at this point either the INTEGER PLUS INTEGER or
# the INTEGER MINUS INTEGER sequence of tokens
# has been successfully found and the method can just
# return the result of adding or subtracting two integers,
# thus effectively interpreting client input
if op.type == PLUS:
result = left.value + right.value
else:
result = left.value - right.value
return result
def main():
while True:
try:
# To run under Python3 replace 'raw_input' call
# with 'input'
text = raw_input('calc> ')
except EOFError:
break
if not text:
continue
interpreter = Interpreter(text)
result = interpreter.expr()
print(result)
if __name__ == '__main__':
main()
Kodları direkt GitHub adresinden indirebilirsiniz. Dosyayı çalıştırıp kendisinden beklendiği gibi çalıştığını görebilirsiniz, yorumlayıcı girdide herhangi bir yerde bulunan boşlukları değerlendirebiliyor; çok haneli sayıları kabul edebiliyor ve bunun yanında toplama yapabildiği gibi bir de çıkarma yapabiliyor.
Bilgisayarımdan programın işleyişine dair bir çıktı:
> python calc2.py
calc> 27 + 3
30
calc> 27 - 7
20
calc>
Part 1’deki versiyona kıyasla değişenler:
- get_next_token fonksiyonunda birtakım değişiklikler yapıldı. Karakter dizisini incelemek için pos işaretçisi, advance adında bir metod ile kontrol ediliyor.
- İki yeni metod: gereksiz boşuk karakterlerini atlamak için skip_whitespace ve çok haneli sayıları almak için integer metodu.
- Görevi işlemi gerçekleştirmek olan expr metodu, INTEGER → PLUS-> INTEGER yapısına ek olarak INTEGER → MINUS → INTEGER yapsını da desteklemesi için modifiye edildi. Artık hem toplama hem de çıkarma işlemleri için gerekli yapıları bulabiliyor.
Part 1’de token ve lexical analyzer adında iki önemli konsept öğrendiniz. Bugün, lexemes, parsing ve parsers hakkında konuşmak istiyorum.
Tokenlerin ne olduğunu halihazırda biliyorsunuz, ama bunun yanında bir de lexemes kavramından bahsetmem gerekiyor. Lexeme dediğimiz kavram nedir? Bir lexeme, token formunda olan karakterlerden oluşmuş bir dizidir. Aşağıdaki resimde bir kaç örnek bulunuyor, umarım aralarındaki bağlantıyı daha anlaşılır kılar.
Şimdi, expr isimli metodu hatırlıyor musunuz? Önceden aritmetik işlemlerin gerçekleştiği yer olduğunu söylemiştim. Ama bir işlemi gerçekleştirmeden önce onun ne olduğunu bulmanız gerekmekte; toplama ya da çıkarma gibi. Bu tam olarak expr metodunun yaptığı iş: metod, get_next_token metodundan aldığı tokenlerde bir yapı buluyor ve bulunan işlemi (burada toplama/çıkarma) gerçekleştiriyor, bunun sonucu ise aritmetik işlemimizin sonucu oluyor.
Tokenler arasından bir yapı bulma olayı ya da farklı bir şekilde ifade etme süreci, parsing olarak adlandırılır. Yorumlayıcılarda bu kısmı yapan bölüme ise parser denir.
Şimdi expr metodunun yorumlayıcımızda hem parsing ve interpreting görevlerini yerine getirdiğini biliyorsunuz - metod ilk olarak INTEGER → PLUS-> INTEGER ya da INTEGER → MINUS → INTEGER yapılarından birini bulmaya çalışıyor (parse), yapı bulunduktan sonra bu yapı değerlendiriyor (interpreting) ve sonucu döndürülüyor.
Aşağıda uğraşabileceğiniz bir kaç egzersiz bulunuyor.
- Yorumlayıcıyı, çarpma işlemini destekler hale getirin.
- Yorumlayıcıyı, bölme işlemini destekler hale getirin.
- Aritmetik ifadeyi değerlendirme olayını modifiye ederek, keyfi sayıda işlemin gerçekleştirilebilmesini sağlayın. Örnek: “9 - 5 + 3 + 11”
Anladıklarınızı Kontrol edin:
- Lexeme nedir?
- Tokenler arasından bir yapı bulma olayı ya da farklı bir şekilde ifade etme sürecine ne ad verilir?
- Yorumlayıcıda parsing görevini yapan bölüme ne ad verilir?
Son
Bu çalışma, bir çeviridir. Yazının İngilizce haline ruslanspivak.com adresinden ulaşabilirsiniz. Toplamda 14 yazıdan oluşan bir seri bu. Zaman buldukça çevirip paylaşmaya çalışacağım.