Ana sayfa Python Programlamaya Giriş 21 – Dinamik kod üretme
Gönderi
İptal

Python Programlamaya Giriş 21 – Dinamik kod üretme

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.

eval

Bu fonksiyon, Python komutları içeren bir dizeyi yorumlayıcıya gönderir ve sonucu geri verir.

In [1]:

1
2
3
eval("2**3 + 4*5")


Out[1]:

1
28

Çalıştırma anında isim alanında bulunan değişkenler de kod dizesi içinde kullanılabilir.

In [2]:

1
2
3
4
5
x = 5

eval("2*x+4")


Out[2]:

1
14

Komut dizesi içindeki değişkeni başka bir değerle kullanmak isterseniz, değişkenleri bir sözlük ile verebilirsiniz.

In [3]:

1
2
3
eval("2*x+4",{"x":10})


Out[3]:

1
24

Ö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.

In [4]:

1
2
3
4
5
6
7
8
9
while(True):

    işlem = input("Bir işlem yazın: ")

    if işlem.strip().lower()=="dur": break

    print(eval(işlem))


1
2
3
4
5
3.142857142857143

1267650600228229401496703205376


Ö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.

In [5]:

1
2
3
list.append.__doc__


Out[5]:

1
'L.append(object) -> None -- append object to end'

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.

In [6]:

1
2
3
dir(list)


Out[6]:

1
2
3
4
5
6
7
8
9
10
11
12
13
['__add__',

 '__class__',

 '__contains__',

 ....

 'remove',

 'reverse',

 'sort']

Altçizgili metodları hariç tutmak istiyoruz, çünkü onlar doğrudan kullanılmaz. Bu amaçla eval‘i bir döngü içinde kullanabiliriz.

In [7]:

1
2
3
4
5
6
7
for metod in dir(list):

    if "__" not in metod:

        print(eval("list."+metod+".__doc__"))


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
L.append(object) -> None -- append object to end

L.clear() -> None -- remove all items from L

L.copy() -> list -- a shallow copy of L

L.count(value) -> integer -- return number of occurrences of value

L.extend(iterable) -> None -- extend list by appending elements from the iterable

L.index(value, [start, [stop]]) -> integer -- return first index of value.

Raises ValueError if the value is not present.

L.insert(index, object) -- insert object before index

L.pop([index]) -> item -- remove and return item at index (default last).

Raises IndexError if list is empty or index is out of range.

L.remove(value) -> None -- remove first occurrence of value.

Raises ValueError if the value is not present.

L.reverse() -- reverse *IN PLACE*

L.sort(key=None, reverse=False) -> None -- stable sort *IN PLACE*


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:

In [8]:

1
2
3
4
5
exec("x=3.1415")

x


Out[8]:

1
3.1415

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.

In [9]:

1
2
3
4
5
6
7
8
9
10
11
12
13
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)


1
2
3
4
5
for x in range(10):

    print(x, 2*x**3-10)


In [10]:

1
2
3
exec(kod)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0 -10

1 -8

2 6

3 44

4 118

5 240

6 422

7 676

8 1014

9 1448


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.

In [11]:

1
2
3
globals()


Out[11]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{'In': ['',

  'eval("2**3 + 4*5")',

  'x = 5\neval("2*x+4")',

 ...

 'formül': '2*x**3-10',

 'get_ipython': <bound method InteractiveShell.get_ipython of <ipykernel.zmqshell.ZMQInteractiveShell object at 0x7f9d822f7b70>>,

 'işlem': 'dur',

 'kod': '\nfor x in range(10):\n    print(x, 2*x**3-10)',

 'metod': 'sort',

 'quit': <IPython.core.autocall.ZMQExitAutocall at 0x7f9d80260630>,

 'x': 9}

In [12]:

1
2
3
4
5
6
7
8
9
10
11
def f(x):

    a = 10

    print(locals())



f(3)


1
2
3
{'a': 10, 'x': 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:

1
2
3
4
5
6
7
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.

In [13]:

1
2
3
exec("print(locals())", None, {"abc": 17, "xyz": "Mehmet"})


1
2
3
{'abc': 17, 'xyz': 'Mehmet'}


In [14]:

1
2
3
exec("print(globals())", None, {"abc": 17, "xyz": "Mehmet"})


1
2
3
{'__name__': '__main__', ... '__builtins__': <module 'builtins' (built-in)>, ... '_i14': '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.

In [15]:

1
2
3
exec("print(globals())", {}, {"abc": 17, "xyz": "Mehmet"})


1
2
3
{'__builtins__': {'__name__': 'builtins', ... 'abs': <built-in function abs>, 'all': <built-in function all>, 'any': <built-in function any>, ...}


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

1
2
3
'x); import os; os.system("touch hello.world");(0,


dizesinin verildiğini varsayalım.

In [16]:

1
2
3
4
5
6
7
8
9
10
11
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)


Bu girdi sonucunda çalıştırılacak kod şöyle olur:

In [17]:

1
2
3
print(kod)


1
2
3
4
5
for x in range(10):

    print(x, x); import os; os.system("touch hello.world");(0,)


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.

In [18]:

1
2
3
exec(kod, {"__builtins__":None}, {"range":range, "print":print})


1
2
3
0 0


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---------------------------------------------------------------------------

ImportError                               Traceback (most recent call last)

<ipython-input-18-d4573d593261> in <module>()

----> 1 exec(kod, {"__builtins__":None}, {"range":range, "print":print})



<string> in <module>()



ImportError: __import__ not found

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.

In [19]:

1
2
3
4
5
6
7
8
9
10
11
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)


In [20]:

1
2
3
4
5
import math

exec(kod, {"__builtins__":None}, {"range":range, "print":print, "sqrt":math.sqrt, "log":math.log})


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0 0.0

1 1.2872726592996706

2 2.4988211106473432

3 3.728009607357671

4 4.991893199734352

5 6.293943592339911

6 7.6340245917967176

7 9.010908615097694

8 10.423067565678043

9 11.868949934707404


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.

Bu gönderi CC BY 4.0 lisansı altındadır.

Python Programlamaya Giriş 20 – Dosya Okuma ve Yazma

Python Programlamaya Giriş 22 – Hata yakalama, try/except