Değerli arkadaşlarım. Sizlerden bir konuda akıl istiyorum.
Eval
Exec
Sumpify nin yaptığı
String bir mayematiksel işlemi
X=“4*5+8-10/2” gibi işlemi matematiksel işlem olarak 23 sonucuna ulaşmak için açık kod ile nasıl yazarım. Pythonda bildiğim şekilde sonuçlandıramadım.
Yazdığınız ifadeye “infix ifade” denir. Yani “operand operator operand” (a işlem b) dizilimiyle yazılmıştır. Bu ifadeyi prefix (işlem a b) veya postfix (a b işlem) ifadeye dönüştürerek bilgisayar tarafından işlenebilir hale getirebilirsiniz. En algoritmik yolu budur. Uyarlaması kolay olsun diye postfix ifadeye dönüştürebilirsiniz.
Zamanında karalamıştım. Belki yardımı dokunur.
Daha da detaylı yazabilirdim ama en azından yol gösterici olabileceğimi düşünüyorum. Bu konuyla ilgili detaylı arama yaptığınızda birden fazla örnek bulabilirsiniz.
Postfix, infix ve örnek koda baktım. İşlem olarak istediğim matematiksel sonuçtu.
Yani hesap makinasında 4 işlemin sonucunda çıkan string sonucu x="4+57-9/3" gibi talebin matematik sonucunu y=4+57-9/3 işlemi 36 sınucu. Stringi matematiksele eval, exec, sympify ile yapıyorum. Derdim eval yada exec işinin sonuçta matematiksel işleme dönüşme kodu. Ezbere iş yapmayı sevmiyorum. Bunları 1982 de basic, cobol, c, c# da yaptım. Ama pythonda bilgi eksiğim var. Değişkenlere baktım ama herhalde gözden kaçırdığım bir dönüşüm eksiğim var. Baştaki type ile bakınca str çıkıyor, işlem float çıkıyor. x deki herbir karateri alıyorum tek ekleme şeklim yine karakter oluyor. Sayıları int çevirsemde operatörler str oluyor ve atanan değişkende hata oluyor. Bunu nasıl yaparım.
Yani s=“5+5” in v=5+5 ==>10 olması.
Merhaba,
Aşağıdaki algoritmayı inceleyin isterseniz. Daha iyi algoritmalar vardır. Ancak benim aklıma aşağıdaki gibi bir yöntem geldi.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
OPS = {
"+": 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 parse_expr(expr: str):
stack = [] # İfadeleri tutacak değişken
term = "" # Terimleri tutacak değişken
for i in range(len(expr)): # İfade üzerinde döngü kurduk
# Eğer i, 0 ise ve
# sırası gelen karakter operator ise ve
# bir önceki karakter operatör değil ise
if i and expr[i] in OPS and expr[i - 1] not in OPS:
if term: # Eğer terim boş değilse
stack += [float(term)] # Yığına terimi dönüştürerek ekle
term = "" # Terimi sıfırla
stack += [expr[i]] # Sonra da operatörü ekle
else:
term += expr[i] # Terime ifadeleri ekle
if term: # Geriye terim kaldıysa
stack += [float(term)] # Bu terimi de dönüştürerek ekle
return stack # Yığını geri döndür.
def calc_stack(stack: list):
# Stacking tek bir ifadeye düşmesi için bir while döngüsü kullanacağız
# stackin uzunluğunun git gide kısalması lazım
while len(stack) > 1:
# İşlemleri, işlem sırasına göre seçmek için bir index tanımladık
index = None # Değeri None
# Örneğin çarpma, eğer bölmeye göre daha düşük bir indiste
# yer alıyorsa önce çarpmayı sonra da bölmeyi yapması lazım
for i, x in enumerate(stack): # stacki tara
if x in OPS: # Eğer karakter bir işlem ise
# index None ise veya
# işlem çarpma veya bölme ise ve
# stack[index]'te toplama ve çıkarma işlemi var ise
if index is None or (x == "^" and stack[index] in "*/+-"):
index = i
if index is None or (x in "*/" and stack[index] in "+-"):
# index'i değiştir
index = i
# Eğer index hala None ise döngüyü durdur
if index is None:
break
# stack[index]: işlem operatörü
# stack[index - 1] operatörün ilk argümanı => a
# stack[index + 1] operatörün ikinci argümanı => b
# OPS[stack[index]](a, b): lambda fonksiyonu
stack[index + 1] = OPS[stack[index]](stack[index - 1], stack[index + 1])
# Şimdi parantezlerle birlikte stackin bir kısmını yok edelim.
del stack[index - 1:index + 1]
return [str(stack[0])]
def eval_expr(expr: str):
# İfade üzerinde değişiklik yapabilmek için
# elemanları bir listeye alalım.
expr = [*expr]
parantheses = [] # Parantezleri tutacak olan liste
# Eğer ifadede hiç parantez yoksa
if "(" not in expr:
# İfadeyi doğrudan calc_expr ve parse_expr ile değerlendir
# Ve sonucu da float olarak al
return float(calc_stack(parse_expr(expr))[0])
# Eğer ifadede parantez varsa
for i in range(len(expr)):
if expr[i] == "(": # Eğer karakter, parantez açma karakteri ise
# Parantezden sonraki ifadenin sıra numarasını listeye ekle
parantheses += [i + 1]
elif expr[i] == ")": # Eğer karakter, parantez kapatma karakteri ise
# Parantez listesindeki son elemanı, listeden düşürerek al
start = parantheses.pop(-1)
# Bu sıra numarası parantezler arasında yer alan son ifadeye ait
end = i
# İfadenin start ile end arasındaki kısmını
# bir str nesnesine dönüştürüyoruz.
sub_expr = "".join(expr[start:end])
# Sonra bu ifadeyi çözüyoruz ve bu çözülmüş ifadeyi
# expr listesindeki parantezli ifadenin yerine koyuyoruz.
expr[start - 1:end + 1] = calc_stack(parse_expr(sub_expr))
# İfadenin tamamını eval_expr fonksiyonuna tekrar gönderiyoruz.
return eval_expr("".join(expr))
# Son olarak çözülmüş ifadeyi tekrar stringe çevirip,
# sonra da float'a çeviriyoruz
return float("".join(expr))
def main():
# Test etmek için ifadeler oluşturalım:
test_cases = [
"3+5^2",
"10-3*2",
"3/-2",
"(-12+30*((28-11+5)*(3-1))/(1--5*(3+2)))",
"4.5+2.1*3",
"(2+3)*(4-1)/2",
"2^3*2"
]
# İfadeleri tek tek alalım:
for expr in test_cases:
# eval_expr fonksiyonu ile eval fonksiyonunu karşılaştıralım.
editted = expr.replace("^", "**")
print(f"{expr} -> {eval_expr(expr) == eval(editted)}")
if __name__ == "__main__":
main()
Merhaba hocam. Anlattığım şey tam olarak da bu işlemi yapıyor. Detaylandırmadığım için, bir postfix ifadenin nasıl hesaplanması gerektiğini anlatmadım.
Bu arada kodumu tekrar inceledim. Eksik kısımlar varmış, mesela 2 ve daha fazla basamaklı sayılarda yanlış sonuçlar veriyor çünkü sonuçları str bazlı yapmışım, hesaplama yaparken her rakamı bağımsız bir sayı olarak alıyor. Ayrıca parantezleri falan da yanlış hesaplamış. Kodumu tekrardan düzenledim. Sizin verdiğiniz ifadelerle çalışan halini atıyorum. Negatif sayılar, float sayılar veya başka operatörler için üzerinde çalışılıp daha da geliştirilebilir. Ek olarak girilen ifadenin doğru bir matematiksel ifade olup olmadığını kontrol etmek de gerekebilir. Burada öyle bir kontrol yok. Onun için de bir lexer ve parser yazılabilir bilmiyorum.
import operator
# gerekli değişkenleri tanımlıyoruz
islemler = {
"+": operator.add,
"-": operator.sub,
"/": operator.truediv, # belki floordiv?
"*": operator.mul,
}
operatorler = {
"+":1,
"-":1,
"/":2,
"*":2,
"(":3,
")":3,
}
infix_ifadeler = [
"4*5+8-10/2",
"4+57-9/3",
"5+5",
"3/(1+1*1)",
"794/(23*34+12)"
]
# tüm işlemleri tek tek yapıyoruz
for infix_ifade in infix_ifadeler:
print("-"*23)
stack = []
postfix_ifade = []
sayi = ""
for i in infix_ifade:
if i.isdigit():
# sayıları bütün olarak elde edebilmek için
sayi += i
else:
if sayi:
postfix_ifade.append(sayi)
sayi = ""
if i == "(":
stack.append(i)
elif i == ")":
while stack and stack[-1] != "(":
postfix_ifade.append(stack.pop())
stack.pop() # açma parantezi kaldırıyoruz
else:
while stack and stack[-1] != "(" and operatorler.get(i) <= operatorler.get(stack[-1]):
postfix_ifade.append(stack.pop())
stack.append(i)
if sayi:
postfix_ifade.append(sayi)
while stack:
postfix_ifade.append(stack.pop())
print(infix_ifade, " ifadesinin postfix sonucu ==>", postfix_ifade)
# postfix ifade yardımıyla işlemin sonucunu hesaplayacağız
for token in postfix_ifade:
if token in islemler:
a = float(stack.pop())
b = float(stack.pop())
stack.append(
islemler[token](b, a)
)
else:
stack.append(token)
print(infix_ifade, "işleminin sonucu:", *stack)
Çıktı:
4*5+8-10/2 ifadesinin postfix sonucu ==> ['4', '5', '*', '8', '+', '10', '2', '/', '-']
4*5+8-10/2 işleminin sonucu: 23.0
-----------------------
4+57-9/3 ifadesinin postfix sonucu ==> ['4', '57', '+', '9', '3', '/', '-']
4+57-9/3 işleminin sonucu: 58.0
-----------------------
5+5 ifadesinin postfix sonucu ==> ['5', '5', '+']
5+5 işleminin sonucu: 10.0
-----------------------
3/(1+1*1) ifadesinin postfix sonucu ==> ['3', '1', '1', '1', '*', '+', '/']
3/(1+1*1) işleminin sonucu: 1.5
-----------------------
794/(23*34+12) ifadesinin postfix sonucu ==> ['794', '23', '34', '*', '12', '+', '/']
794/(23*34+12) işleminin sonucu: 1.0
Ek bilgi olarak, infix postfix ve prefix ifadeler bir binary tree üzerinde gösterilebilir ve ağaç üzerinde gezinmeye göre ifadeler oluşturulabilir. Mesela
(/)
/ \
(3) (+)
/ \
(1) (*)
/ \
(1) (1)
infix olarak sol - kök - sağ şeklinde okunursa
3 / 1 + 1 * 1
postfix olarak sol - sağ - kök şeklinde okunursa
3 1 1 1 * + /
prefix ifade kök - sol - sağ şeklinde okunursa
/ 3 + 1 * 1 1
olarak elde edilir.
"3/-2"
ifadesini veya "(-12+30*((28-11+5)*(3-1))/(1--5*(3+2)))"
ifadesini çözmeye çalışırken hata veriyor.
-----------------------
3/-2 ifadesinin postfix sonucu ==> ['3', '/', '2', '-']
Traceback (most recent call last):
File "/home/tanberk/./deneme.py", line 76, in <module>
b = float(stack.pop())
^^^^^^^^^^^
IndexError: pop from empty list
İlginize teşekkürler. Bilmediğim bir konuyu da öğrenmiş oldum. Çok teşekkürler…
İzninizle ben de paylaştığım kodlarla alakalı ek bilgi vereyim:
Yukarıdaki OPS
sözlüğüne ^
işareti ile temsil edilen **
= üst alma işlemini ekledim.
Sonra da calc_expr
fonksiyonuna şu koşul cümlesini ekledim:
if index is None or (x == "^" and expr[index] in "*/+-"):
index = i
Burada ^
işlemi, */+-
işlemlerini önceler.
Sadece dört işleme göre önceleme sırası oluştururken aşağıdaki koşul cümlesi eklenmişti.
if index is None or (x in "*/" and expr[index] in "+-"):
# index'i değiştir
index = i
Burada ise */
işlemleri +-
işlemlerini önceler.
OPS
’a her yeni işlem eklemek istediğimizde, calc_expr
fonksiyonunda da ilgili işlemin öncelik sırasını tanımlamamız gerekiyor (şayet bir önceliği varsa). Üs alma işlemi, diğer bütün işlemlerden daha önce çalışsın istiyorsak, yukarıdaki gibi calc_expr
fonksiyonuna o durumun koşulunu da ekleriz.
Evet hocam zaten yukarıda da belirtmiştim negatif sayılarda işlem kontrolü yok o yüzden eklemelerle geliştirilebilir diye. Vaktim olmadığı için çok ekleme yapamadım.
Teşekkür ederim… ilginiz için sağolun… Bir yıl önce kızımın okuldan getirdiği bir python kodu ile serüven başladı. Geçmiş 1982 de ibm/23 lerde başlayan son olarak c# ile biten ve yeter dedipim bir serüven maalesef takıldı hayatıma. Yazbel.com ve aldığım bir kitapla başlayan serüven ve acaba bu nasıl olur derken çok geniş bir dünyanın içine girdim. Hala %1 lerde olan bir serüven. Ama çok güzel bir grup ve uzanan yardımlar ile zevkli bir çalışma oluyor. Tekrar teşekkürler…
O kısmı aklımdan çıkmış ya. Kusuruma bakma.
Ufak eklemeler:
Eskiden bu isler flex + yacc veya bison ile yapilir idi.
Yakin zamanda compiler dersi almis olan varsa bu gerecleri kullanmis olmasi muhtemel.
Biraz daha acarsak:
Ifadeleri once token serisine cevirip sonra parse etmek kolay bir yol.
Yani 4*5+8-10/2
gibi bir ifade:
[sayi(4), CARPMA, sayi(5), TOPLAMA, sayi(8)...]
gibi bir token serisine donusuyor.
Bundan sonra istenen ifade agacina donmek icin parse ediyoruz. Parser konusu oldugunca karisik. En basidinden, “islemleri soldan saga yap” gibi kurallari duz bir loop’la yapabilirken, “parantezlere oncelik ver, sonra uslere, sonra carpmaya, vs” dedigimiz noktada daha karmasik seyler devreye girebiliyor.
Ozellikle icice parantezleri islemek icin bir miktar hafiza gerekiyor. (((((((8))))
gibi bir ifadenin dogru olup olmadigini bilmek icin bile (
'larin sayisini tutmak gerekiyor mesela. )
'larin sayisi da ayni olmali cunku.
Bu arada bu tokenizer + parser ayrimi, C++'da hede<hodo<foo> >
derken araya bosluk koymamiz gerekmesinin nedeni. Tokenization, gramerden tamamen ayri ve once calisan bir adim oldugu icin, oradaki >
'lerin tur parametre kapamasi oldugunu bilmiyor, >>
'i gordugu yerde KUCUKTUR token’i koyuyor, TUR_PARAMETRESI_KAPA, TUR_PARAMETRESI_KAPA yapacagina. Bir suru dilde benzer bir kisitlama var.
Hafizanin ucuz oldugu su aralar, oyuncak olarak yapilan parser’larin recursive descent olmasi daha mantikli olabilir. Hatta bir soyutlama daha koyarak parser combinator kullanmak, hatta bir tane daha koyup monadik parser combinator kullanmak isi neredeyse keyifli bir hale getiriyor. (Cok guzel bir ornegi vardi, fakat linkini bulamadim. Arastirmaya yonlendirmek durumundayim malesef.) Bu arada, bu noktada tokenization da parser’in icine rahatca koyulabiliyor.
Ilgi olursa baska bir konuda konusulabilir.
Teşekkürler. Söyledikleriniz geniş konular ki bunlardan bir haber kişiyim. Hani düz yazılımcı derler ya o türden biriyim. Zamanıbsa Bordro, muhasebe finans, stok kontrol gibi uygulama yazılımları yaptım. Sonrasında derinlere ineyim dedim ama onunda bie eğitim ve zaman olduğunu gördüm. Yinede ifade edilen konulara bakacağım. İlginize teşekkürler.
Burada şöyle düşündüm:
Döngü esnasında "("
karakterine denk gelindiğinde ilgili indeks numarasını bir parantez indeks listesine kaydet. Sonra da her ")"
karakterine denk gelindiğinde de, parantez listesine eklenen son "("
karakterinin indeksini al. Sonra da parantez listesinin son elemanını düşür.
if expr[i] == "(": # Eğer karakter, parantez açma karakteri ise
# Parantezden sonraki ifadenin sıra numarasını listeye ekle
parantheses += [i + 1]
elif expr[i] == ")": # Eğer karakter, parantez kapatma karakteri ise
# Parantez listesindeki son elemanı, listeden düşürerek al
start = parantheses.pop(-1)
# Bu sıra numarası parantezler arasında yer alan son ifadeye ait
end = i
Parantez kapayan karakterlerin indislerini bir listede tutmadım. Çünkü ")"
karakterine sıra gelir gelmez, ifadeyi çözmek ve bir parantez çiftini ortadan kaldırmak mantıklı gelmişti.
start
ile end
arasında kalan kısım en içteki paranteze karşılık geliyor. Yani parantez çözme işini, içten dışa doğru yapmak daha kolay geldi bana.
Python’da da **
işlemini eklerken ^
sembolünü kullandım. Çünkü **
işaretini, *
işaretinden ayırmakla uğraşmak istemedim.
<<
(left shift) ile <
(büyüktür) işaretleriyle de çalışan, sınırılı bir eval fonksiyonu yazmak istesek, burada da benzer bir sorun oluşacaktı. Birinin diğerinden farklı olduğunu bir önceki veya bir sonraki indisteki elemana bakarak kontrol etmek zorunda kalırdık.
Eğer eval
’i, C++ söz dizimini çalıştıran bir fonksiyon olarak yazmak istesek, hede<hodo<foo> >
ifadesi senin de belirttiğin gibi söz diziminden kaynaklanan bir soruna neden olur. Ve işimiz baya zorlaşırdı o zaman.
Mesela *
işareti; hem Python, hem de C/C++ dillerinde birden fazla kavrama karşılık geliyor. İşaretin hangi bağlamda kullanıldığını çözümlemeye çalışmak biraz uğraştırırdı.
(Bir işaretin tek bir anlama geldiği domain-specific diller de var gerçi. Ama işaretin polimorfik olması, yani bir dilde bir çok bağlamda kullanılabilmesi, o dilin soyutlama özelliğini artıran bir etmen de aynı zamanda.)
Ama sadece matematiksel işlemleri çalıştıran kısıtlı bir eval
fonksiyonunda gramerden kaynaklanan bir sorun olmazdı.
Bu mevzu yanlış hatırlamıyorsam yığıt tipi bir yapı ile kontrol ediliyordu. FILO (hafızaya ilk kaydedilen son çıkarılır ya da son kaydedilen ilk çıkar) mantığı ile yığıt hafızamıza sırasıyla diyelim “(((” karakterleri eklendi. Ardından çeşitli rakamlar ve işlem operatörleri var diyelim. Mesela “5+3” olsun. Ardından “)” karakteri gelince yığıt “(((5+3)” olacak. İşte bu son gelen “)” karakteri yığıta girdiği an sırasıyla ),3,+,5,( karakterleri yıgıttan çıkarılır, 5 ve 3 toplanır. Yıgıtta “((” karakterleri kalır. Yığıt son halde “((8” karakterlerini barındırırken yeni veri girişi beklenir. Yiğit boşalmış ise son parantez kapama girilmiş, dolayısıyla işlem sonlanmış olur.
Algoritmik düşünce gücünü çalıştırma adına faydalı bir konu olmuş, yorum yazan bilgilerimizi artıran tüm üyelere teşekkürler.
Aynen, burada bahsettiğim parantez çözme algoritması FILO (first in last out) mekanizmasına benziyor aslında. Bu yaklaşım, iç içe geçmiş parantezleri çözmek konusunda bana daha kullanışlı gelmişti.
İçteki parantezi çöz, sonra sonucu bir üst paranteze taşı. Sonra da bir üst parantezi çöz, sonucu daha üst seviyedeki paranteze taşı; bu şekilde parantezler içten dışa doğru çözülsün.
Teşekkür ederim. Detaylı düşünerek çoklu işlemi önce parantezde parçalayıp sonrada + ve - de parçalayıp öncelikli */ işlemini yapıp deneyeceğim.
Monadic recursion
Senin dediğin gibi; her bir parantezi bir monad
olarak değerlendiriyoruz. Parantezleri çözmek demek; parçası, bütününe benzeyen yapıları çözmek demek aslında. nested
bir listeyi, flat
hale getirme işlemine benziyor. Bir veriyi genellikle monad olabilecek hale getirmek, recursive
çağrıları kullanışlı bir yöntem haline getiriyor. Dediğin gibi bu monadları çözmek çok zevkli bir uğraş.
Hm, bu baglantiyi kuramadim.
Benim dedigim, monad’lar combinatorial parser’larda ise yariyor. Surada guzel bir paper var, ama simdi tekrar bakinca combinatory parser yazmak kadar buyuk bir olay degil, uzerine monadik islem eklemek.