Python dinamik tabir edilen dillerden biridir; programdaki nesneleri önceden bildirmeniz gerekmez, program çalıştıkça işlenen komutlar o anda yeni nesneler üretir. Bu dinamiklik sayesinde, dize olarak verilmiş Python komutlarını da işleyebilir, hatta program yazan programlar yazabiliriz.
Bu işlemi yapmak için iki Python fonksiyonu vardır: eval()
ve exec()
Dizinin bütün yazılarına erişmek için Python Programlamaya Giriş kategorimize bakabilirsiniz. Bu dizideki yazılar ayrıca Jupyter defterleri halinde GitHub depomuzda da mevcut.
eval
Bu fonksiyon, Python komutları içeren bir dizeyi yorumlayıcıya gönderir ve sonucu geri verir.
eval("2**3 + 4*5")
Çalıştırma anında isim alanında bulunan değişkenler de kod dizesi içinde kullanılabilir.
x = 5
eval("2*x+4")
Komut dizesi içindeki değişkeni başka bir değerle kullanmak isterseniz, değişkenleri bir sözlük ile verebilirsiniz.
eval("2*x+4",{"x":10})
Örnek: Basit hesap makinesi
Kullanıcıdan tek tek matematiksel ifadeler alıp sonucu yazan bir programcık yazalım. Kullanıcı “dur” yazdığında program sona ersin.
while(True):
işlem = input("Bir işlem yazın: ")
if işlem.strip().lower()=="dur": break
print(eval(işlem))
(Bu programı Jupyter defterinde çalıştırıyorsanız, Python çekirdeğiyle iletişimindeki tamponlama sebebiyle işlemler ve sonuçlar doğru sırada çıkmayabilir.)
Örnek: Yardım belgeleri özetleri
Nesnelerin yardım belgelerini daha önce kullanmıştık. Bir metodla ilgili bilgi almak için help()
komutunu kullandığımızda o metodun (bir fonksiyon nesnesidir) __doc__
isimli özelliği ekrana basılır. Buna doğrudan da erişebiliriz.
list.append.__doc__
Diyelim bir nesne sınıfı altında tanımlanmış bütün metodların kısa tarifini (belge dizesini) ekrana dökmek istiyoruz. Ama bir sınıf altında, daha özel amaçlı metodlar da bulunur. Bunlar başlarında ve sonlarında bir çift altçizgi kullanırlar.
dir(list)
Altçizgili metodları hariç tutmak istiyoruz, çünkü onlar doğrudan kullanılmaz. Bu amaçla eval
‘i bir döngü içinde kullanabiliriz.
for metod in dir(list):
if "__" not in metod:
print(eval("list."+metod+".__doc__"))
exec
Değer döndüren ifadeleri eval()
ile işletebiliriz, ama bir ifade (expression) olmayan, yani değer döndürmeyen komutları (söz gelişi fonksiyon tanımları, döngüler, atamalar vb.) çalıştırmak için exec()
fonksiyonuna ihtiyacımız var.
Örnek olarak, bir değişken ataması yapalım:
exec("x=3.1415")
x
Bir formülde değişkene 0-9 arası değerler vererek bir tablo oluşturan bir kod yazalım. Formülü kullanıcıdan alalım.
değişken = "x" # Formülde kullanılacak değişken.
formül = input("Bir matematiksel formül yazın: ")
kod = """
for {0} in range(10):
print({0}, {1})""".format(değişken, formül)
print(kod)
exec(kod)
Farkedeceğiniz gibi, değişken
‘in değeri "x"
olduğu için formülde de x
karakterini kullanmamız gerekiyor. Ama değişken
‘e farklı bir değer atayarak formülde farklı bir değişken adı kullanmamız mümkün olur.
Global ve yerel değişkenler
eval
/exec
fonksiyonları, işletilecek kodu barındıran dizenin yanı sıra iki parametre daha alırlar: globals ve locals. Bu parametreler özellikle belirtilmezse, eval
/exec
kodunda yorumlayıcının o andaki durumunda tanımlanmış olan bütün isimler kullanılabilir.
Yerel değişkenler bir fonksiyon içinden tanımlı olan, o fonksiyonun dışında tanınmayan isimlerdir. Global değişkenler ise bütün fonksiyonların erişebileceği değişkenlerdir. Yerel isimlere locals()
, global isimlere ise globals()
komutlarıyla ulaşılabilir. Bu komutlar değişken isimleriyle değerlerini eşleştiren birer sözlük döndürür.
globals()
def f(x):
a = 10
print(locals())
f(3)
eval
/exec
ile bir kod parçası çalıştırırken bu global ve yerel değişkenleri sınırlandırabiliriz. Bu fonksiyonların genel kullanımı şöyledir:
eval(source, globals=None, locals=None)
exec(source, globals=None, locals=None)
Burada globals
ve locals
parametreleri olarak global ve yerel değişkenleri tutan birer sözlük koyabiliriz.
exec("print(locals())", None, {"abc": 17, "xyz": "Mehmet"})
exec("print(globals())", None, {"abc": 17, "xyz": "Mehmet"})
globals yerine boş bir sözlük koyarsak sadece Python dilinin parçası olarak tanımlanmış isimlere erişilebilir.
exec("print(globals())", {}, {"abc": 17, "xyz": "Mehmet"})
Bunlara bile erişimi kapatmamız mümkündür, aşağıdaki bölümde göreceğimiz gibi.
Güvenlik
Dışarıdan alınan bir kodu çalıştırmak her zaman risklidir. exec()
ve eval()
fonksiyonlarının bilgisayarınıza bir kapı açtığını unutmayın.
Tehlikeyi örneklemek için, yukarıdaki örneği tekrar ele alalım. Siz bir formül beklerken, kötü niyetli bir kullanıcı işletim sisteminizi yönetecek bir komutu bu formülle beraber verebilir. Meselâ, formül sorulduğunda
'x); import os; os.system("touch hello.world");(0,
dizesinin verildiğini varsayalım.
değişken = "x" # Formülde kullanılacak değişken.
formül = 'x); import os; os.system("touch hello.world");(0,'
kod = """
for {0} in range(10):
print({0}, {1})""".format(değişken, formül)
print(kod)
Burada kötü niyetli kullanıcı beklenen formülü verdikten sonra parantezi kapatmış ve işletim sistemine yönelik komutlar eklemiş. (Sondaki (0,
kısmı, kalıpta bulunan sağ parantezin sentaks hatası vermemesi için, onu etkisiz eleman haline dönüştürüyor.)
Bu kodu exec(kod)
ile çalıştırdığınızda ekrana sayılar tablosu çıkmasının yanı sıra, bu programı çalıştırdığınız dizinin altında hello.world isimli boş bir dosya yaratıldığını göreceksiniz (Linux kullanıyorsanız). Yani program işletim sisteminize erişebildi. Kötü niyetli bir saldırgan aynı yöntemle diskinizi silebilir, şifrelerinizi çalabilir, virüs yerleştirebilir.
Bu risklere karşı alınabilecek kısmi tedbirler vardır. En yaygın olanı, exec
‘in çalıştığı sanal ortamdaki değişkenleri, globals ve locals parametreleri kullanarak düzenlemektir. Sözgelişi aşağıda, globals parametresi olarak {"__builtins__":None}
vermekle Python’un öntanımlı fonksiyonlarını kapatırız. Böylelikle import
ile bir modül yüklenmesini ve işletim sistemine ulaşılmasını engelleriz. Bu işlem range
ve print
fonksiyonlarını da kapatır, o yüzden locals parametresine bunların tanımlarını içeren bir sözlük veririz.
exec(kod, {"__builtins__":None}, {"range":range, "print":print})
Bu yeni düzende import
fonksiyonu tanınmadığı için exec()
çağrısı bir hata verdi ve sızma engellendi. Aynı kodu beklenen şekilde bir girdiyle çalıştırdığınızda ise sorun yaşamazsınız.
Matematik kütüphanesindeki fonksiyonları kullanan işlemler yapmak istiyorsanız, gereken fonksiyonlardan oluşan bir “beyaz liste” oluşturabilirsiniz.
değişken = "x" # Formülde kullanılacak değişken.
formül = "x * sqrt(x+1)/log(x+2)"
kod = """
for {0} in range(10):
print({0}, {1})""".format(değişken, formül)
import math
exec(kod, {"__builtins__":None}, {"range":range, "print":print, "sqrt":math.sqrt, "log":math.log})
Kendi kullanacağınız programlar veya bir masaüstü uygulaması için fazla tedbir almak gerekmeyebilir. Yanlış veya kötü niyetli bir kullanım sadece kullanıcıya zarar verecektir. Ama bir web uygulaması yazıyorsanız güvenliğe çok daha fazla dikkat etmelisiniz. Web güvenliğinde uzmanlaşan kaynaklardan daha ayrıntılı bilgi edinebilirsiniz.
Özetle, dinamik olarak üretilen bir kodu işletmek için exec
/eval
kullanabilirsiniz. Bunun yararlı, hatta elzem olduğu çeşitli durumlar vardır. Söz gelişi
- Python sözdizimiyle yazılmış bir ifadeyi doğrudan alıp işlemek,
- Bir konfigürasyon dosyasını herhangi bir “parsing” işlemine tabi tutmadan yorumlamak,
- Bir programın kaynak koduna dokunmadan ek modüller yüklemek.
exec
/eval
ilk bakışta çok hoş görünseler de çok sık kullanılmamalıdırlar, bazı sakıncaları vardır.
- Kodu okumayı zorlaştırır. Programı anlamak istiyorsak kaynak koduna ek olarak, çalıştırılmak üzere alınacak kodun ne olduğunu da bilmeliyiz.
- Kodu test etmeyi, hataları bulmayı zorlaştırır.
- Güvenlik açığı oluşturur.
exec
/eval
fonksiyonlarının işe yaradığı durumlar vardır, ama probleminizi önce normal kod kullanarak çözmeye çalışın.