İleri Düzey Fonksiyonlar¶
Buraya gelinceye kadar fonksiyonlara ilişkin epey söz söyledik. Artık Python programlama dilinde fonksiyonlara dair hemen her şeyi bildiğimizi rahatlıkla söyleyebiliriz. Zira bu noktaya kadar hem fonksiyonların temel (ve orta düzey) özelliklerini öğrendik, hem de ‘gömülü fonksiyon’ kavramını ve gömülü fonksiyonların kendisini bütün ayrıntılarıyla inceledik. Dolayısıyla yazdığımız kodlarda fonksiyonları oldukça verimli bir şekilde kullanabilecek kadar fonksiyon bilgisine sahibiz artık.
Dediğimiz gibi, fonksiyonlara ilişkin en temel bilgileri edindik. Ancak fonksiyonlara dair henüz bilmediğimiz şeyler de var. Ama artık Python programlama dilinde geldiğimiz aşamayı dikkate alarak ileriye doğru bir adım daha atabilir, fonksiyonlara dair ileri düzey sayılabilecek konulardan da söz edebiliriz.
İlk olarak ‘lambda fonksiyonlarını’ ele alalım.
Lambda Fonksiyonları¶
Şimdiye kadar Python programlama dilinde fonksiyon tanımlamak için hep def adlı bir ifadeden yararlanmıştık. Bu bölümde ise Python programlama dilinde fonksiyon tanımlamamızı sağlayacak, tıpkı def gibi bir ifadeden daha söz edeceğiz. Fonksiyon tanımlamamızı sağlayan bu yeni ifadeye lambda denir. Bu ifade ile oluşturulan fonksiyonlara ise ‘lambda fonksiyonları’…
Bildiğiniz gibi Python’da bir fonksiyonu def ifadesi yardımıyla şöyle tanımlıyoruz:
>>> def fonk(param1, param2):
... return param1 + param2
Bu fonksiyon, kendisine verilen parametreleri birbiriyle toplayıp bize bunların toplamını döndürüyor:
>>> fonk(2, 4)
6
Peki aynı işlemi lambda fonksiyonları yardımıyla yapmak istersek nasıl bir yol izleyeceğiz?
Dikkatlice bakın:
>>> fonk = lambda param1, param2: param1 + param2
İşte burada tanımladığımız şey bir lambda fonksiyonudur. Bu lambda fonksiyonunu da tıpkı biraz önce tanımladığımız def fonksiyonu gibi kullanabiliriz:
>>> fonk(2, 4)
6
Gördüğünüz gibi lambda fonksiyonlarını tanımlamak ve kullanmak hiç de zor değil.
Lambda fonksiyonlarının neye benzediğinden temel olarak bahsettiğimize göre artık biraz daha derine inebiliriz.
Lambda fonksiyonları Python programlama dilinin ileri düzey fonksiyonlarından biridir. Yukarıdaki örnek yardımıyla bu lambda fonksiyonlarının nasıl bir şey olduğunu gördük. Esasında biz buraya gelene kadar bu lambda fonksiyonlarını hiç görmemiş de değiliz. Hatırlarsanız daha önceki derslerimizde şöyle bir örnek kod yazmıştık:
harfler = "abcçdefgğhıijklmnoöprsştuüvyz"
çevrim = {i: harfler.index(i) for i in harfler}
isimler = ["ahmet", "ışık", "ismail", "çiğdem",
"can", "şule", "iskender"]
print(sorted(isimler, key=lambda x: çevrim.get(x[0])))
Burada sorted()
fonksiyonunun key parametresi içinde kullandığımız ifade
bir lambda fonksiyonudur:
lambda x: çevrim.get(x[0])
Peki lambda fonksiyonları nedir ve ne işe yarar?
Lambda fonksiyonlarını, bir fonksiyonun işlevselliğine ihtiyaç duyduğumuz, ama
konum olarak bir fonksiyon tanımlayamayacağımız veya fonksiyon tanımlamanın zor
ya da meşakkatli olduğu durumlarda kullanabiliriz. Yukarıdaki örnek kod, bu
tanıma iyi bir örnektir: sorted()
fonksiyonunun key parametresi bizden bir
fonksiyon tanımı bekler. Ancak biz elbette oraya def ifadesini kullanarak
doğrudan bir fonksiyon tanımlayamayız. Ama def yerine lambda ifadesi
yardımıyla key parametresi için bir lambda fonksiyonu tanımlayabiliriz.
Eğer yukarıdaki kodları ‘normal’ bir fonksiyonla yazmak isteseydik şu kodları kullanabilirdik:
harfler = "abcçdefgğhıijklmnoöprsştuüvyz"
çevrim = {i: harfler.index(i) for i in harfler}
isimler = ["ahmet", "ışık", "ismail", "çiğdem",
"can", "şule", "iskender"]
def sırala(eleman):
return çevrim.get(eleman[0])
print(sorted(isimler, key=sırala))
Burada lambda fonksiyonu kullanmak yerine, sırala()
adlı bir fonksiyon
kullandık.
Eğer yukarıda ‘lambda’ ile yazdığımız örneği sırala()
fonksiyonu ile
yazdığımız örnekle kıyaslarsanız lambda fonksiyonlarında hangi parçanın neye
karşılık geldiğini veya ne anlama sahip olduğunu rahatlıkla anlayabilirsiniz.
Gelin bir örnek daha verelim:
Diyelim ki bir sayının çift sayı olup olmadığını denetleyen bir fonksiyon yazmak istiyorsunuz. Bunun için şöyle bir fonksiyon tanımlayabileceğimizi biliyorsunuz:
def çift_mi(sayı):
return sayı % 2 == 0
Eğer çift_mi()
fonksiyonuna parametre olarak verilen bir sayı çift ise
fonksiyonumuz True çıktısı verecektir:
print(çift_mi(100))
True
Aksi halde False çıktısı alırız:
print(çift_mi(99))
False
İşte yukarıdaki etkiyi lambda fonksiyonları yardımıyla da elde edebiliriz.
Dikkatlice bakın:
>>> çift_mi = lambda sayı: sayı % 2 == 0
>>> çift_mi(100)
True
>>> çift_mi(99)
False
Başka bir örnek daha verelim. Diyelim ki bir liste içindeki bütün sayıların karesini hesaplamak istiyoruz. Elimizdeki liste şu:
>>> l = [2, 5, 10, 23, 3, 6]
Bu listedeki sayıların her birinin karesini hesaplamak için şöyle bir şey yazabiliriz:
>>> for i in l:
... print(i**2)
4
25
100
529
9
36
Veya şöyle bir şey:
>>> [i**2 for i in l]
[4, 25, 100, 529, 9, 36]
Ya da map()
fonksiyonuyla birlikte lambda’yı kullanarak şu kodu
yazabiliriz:
>>> print(*map(lambda sayı: sayı ** 2, l))
4 25 100 529 9 36
Son örnekte verdiğimiz lambda’lı kodu normal bir fonksiyon tanımlayarak şöyle de yazabilirdik:
>>> def karesi(sayı):
... return sayı ** 2
...
>>> print(*map(karesi, l))
4 25 100 529 9 36
Sözün özü, mesela şu kod:
lambda x: x + 10
Türkçede şu anlama gelir:
'x' adlı bir parametre alan bir lambda fonksiyonu tanımla. Bu fonksiyon, bu
'x parametresine 10 sayısını eklesin.
Biz yukarıdaki örneklerde lambda fonksiyonunu tek bir parametre ile tanımladık. Ama elbette lambda fonksiyonlarının birden fazla parametre de alabileceğini de biliyorsunuz.
Örneğin:
>>> birleştir = lambda ifade, birleştirici: birleştirici.join(ifade.split())
Burada lambda fonksiyonumuz toplam iki farklı parametre alıyor: Bunlardan ilki
ifade, ikincisi ise birleştirici. Fonksiyonumuzun gövdesinde ifade
parametresine split()
metodunu uyguladıktan sonra, elde ettiğimiz parçaları
birleştirici parametresinin değerini kullanarak birbirleriyle birleştiriyoruz.
Yani:
>>> birleştir('istanbul büyükşehir belediyesi', '-')
'istanbul-büyükşehir-belediyesi'
Eğer aynı işlevi ‘normal’ bir fonksiyon yardımıyla elde etmek isteseydik şöyle bir şey yazabilirdik:
>>> def birleştir(ifade, birleştirici):
... return birleştirici.join(ifade.split())
...
>>> birleştir('istanbul büyükşehir belediyesi', '-')
'istanbul-büyükşehir-belediyesi'
Yukarıdaki örneklerin dışında, lambda fonksiyonları özellikle grafik arayüz çalışmaları yaparken işinize yarayabilir. Örneğin:
import tkinter
import tkinter.ttk as ttk
pen = tkinter.Tk()
btn = ttk.Button(text='merhaba', command=lambda: print('merhaba'))
btn.pack(padx=20, pady=20)
pen.mainloop()
Not
Bu kodlardan hiçbir şey anlamamış olabilirsiniz. Endişe etmeyin. Burada amacımız size sadece lambda fonksiyonlarının kullanımını göstermek. Bu kodlarda yalnızca lambda fonksiyonuna odaklanmanız şimdilik yeterli olacaktır. Eğer bu kodları çalıştıramadıysanız https://forum.yazbel.com/ adresinde sorununuzu dile getirebilirsiniz.
Bu kodları çalıştırıp ‘merhaba’ düğmesine bastığınızda komut satırında ‘merhaba’ çıktısı görünecektir. Tkinter’de fonksiyonların command parametresi bizden parametresiz bir fonksiyon girmemizi bekler. Ancak bazen, elde etmek istediğimiz işlevsellik için oraya parametreli bir fonksiyon yazmak durumunda kalabiliriz. İşte bunun gibi durumlarda lambda fonksiyonları faydalı olabilir. Elbette yukarıdaki kodları şöyle de yazabilirdik:
import tkinter
import tkinter.ttk as ttk
pen = tkinter.Tk()
def merhaba():
print('merhaba')
btn = ttk.Button(text='merhaba', command=merhaba)
btn.pack(padx=20, pady=20)
pen.mainloop()
Burada da lambda yerine isimli bir fonksiyon tanımlayıp, command parametresine doğrudan bu fonksiyonu verdik.
Bütün bu örneklerden gördüğünüz gibi, lambda fonksiyonları son derece pratik araçlardır. Normal, isimli fonksiyonlarla elde ettiğimiz işlevselliği, lambda fonksiyonları yardımıyla çok daha kısa bir şekilde elde edebiliriz. Ancak lambda fonksiyonları normal fonksiyonlara göre biraz daha okunaksız yapılardır. O yüzden, eğer lambda fonksiyonlarını kullanmaya mecbur değilseniz, bunların yerine normal fonksiyonları veya yerine göre liste üreteçlerini tercih edebilirsiniz.
Özyinelemeli (Recursive) Fonksiyonlar¶
Bu bölümde, lambda fonksiyonlarının ardından, yine Python’ın ileri düzey konularından biri olan ‘özyinelemeli fonksiyonlar’dan söz edeceğiz. İngilizcede recursive functions olarak adlandırılan özyinelemeli fonksiyonların, Python programlama dilinin anlaması en zor konularından biri olduğu söylenir. Ama bu söylenti sizi hiç endişelendirmesin. Zira biz burada bu çapraşık görünen konuyu size olabildiğince basit ve anlaşılır bir şekilde sunmak için elimizden gelen bütün çabayı göstereceğiz.
O halde hemen başlayalım…
Şimdiye kadar Python’da pek çok fonksiyon gördük. Bu fonksiyonlar kimi zaman Python programcılarınca tanımlanıp dile entegre edilmiş ‘gömülü fonksiyonlar’ (builtin functions) olarak, kimi zamansa o anda içinde bulunduğumuz duruma ve ihtiyaçlarımıza göre bizzat kendimizin tanımladığı ‘el yapımı fonksiyonlar’ (custom functions) olarak çıktı karşımıza.
Şimdiye kadar öğrendiğimiz bütün bu fonksiyonların ortak bir noktası vardı. Bu ortak nokta, şu ana kadar fonksiyonları kullanarak yaptığımız örneklerden de gördüğünüz gibi, bu fonksiyonlar yardımıyla başka fonksiyonları çağırabiliyor olmamız. Örneğin:
def selamla(kim):
print('merhaba', kim)
Burada selamla()
adlı bir fonksiyon tanımladık. Gördüğünüz gibi bu fonksiyon
print()
adlı başka bir fonksiyonu çağırıyor. Burada sıradışı bir şey yok.
Dediğimiz gibi, şimdiye kadar zaten hep böyle fonksiyonlar görmüştük.
Python fonksiyonları, yukarıdaki örnekte de gördüğünüz gibi, nasıl başka fonksiyonları çağırabiliyorsa, aynı şekilde, istenirse, kendi kendilerini de çağırabilirler. İşte bu tür fonksiyonlara Python programlama dilinde ‘kendi kendilerini yineleyen’, veya daha teknik bir dille ifade etmek gerekirse ‘özyinelemeli’ (recursive) fonksiyonlar adı verilir.
Çok basit bir örnek verelim. Diyelim ki, kendisine parametre olarak verilen bir karakter dizisi içindeki karakterleri teker teker azaltarak ekrana basan bir fonksiyon yazmak istiyorsunuz. Yani mesela elinizde ‘istihza’ adlı bir karakter dizisi var. Sizin amacınız bu karakter dizisini şu şekilde basan bir fonksiyon yazmak:
istihza
stihza
tihza
ihza
hza
za
a
Elbette bu işi yapacak bir fonksiyonu, daha önce öğrendiğiniz döngüler ve başka yapılar yardımıyla rahatlıkla yazabilirsiniz. Ama isterseniz aynı işi özyinelemeli fonksiyonlar yardımıyla da yapabilirsiniz.
Şimdi şu kodlara dikkatlice bakın:
def azalt(s):
if len(s) == 0:
return s
else:
print(s)
return azalt(s[1:])
print(azalt('istihza'))
Bu kodlar bize yukarıda bahsettiğimiz çıktıyı verecek:
istihza
stihza
tihza
ihza
hza
za
a
Fonksiyonumuzu yazıp çalıştırdığımıza ve bu fonksiyonun bize nasıl bir çıktı verdiğini gördüğümüze göre fonksiyonu açıklamaya geçebiliriz.
Bu fonksiyon ilk bakışta daha önce öğrendiğimiz fonksiyonlardan çok da farklı görünmüyor aslında. Ama eğer fonksiyonun son kısmına bakacak olursanız, bu fonksiyonu daha önce öğrendiğimiz fonksiyonlardan ayıran şu satırı görürsünüz:
return azalt(s[1:])
Gördüğünüz gibi, burada azalt()
fonksiyonu içinde yine azalt()
fonksiyonunu çağırıyoruz. Böylece fonksiyonumuz sürekli olarak kendi kendini
yineliyor. Yani aynı fonksiyonu tekrar tekrar uyguluyor.
Peki ama bunu nasıl yapıyor?
Nasıl bir durumla karşı karşıya olduğumuzu daha iyi anlamak için yukarıdaki kodları şu şekilde yazalım:
def azalt(s):
if len(s) < 1:
return s
else:
print(list(s))
return azalt(s[1:])
Burada fonksiyonun her yinelenişinde, özyinelemeli fonksiyona parametre olarak giden karakter dizisinin nasıl değiştiğini birazcık daha net olarak görebilmek için karakter dizisi içindeki karakterleri bir liste haline getirip ekrana basıyoruz:
print(list(s))
Bu kodları çalıştırdığımızda şu çıktıyı alacağız:
['i', 's', 't', 'i', 'h', 'z', 'a']
['s', 't', 'i', 'h', 'z', 'a']
['t', 'i', 'h', 'z', 'a']
['i', 'h', 'z', 'a']
['h', 'z', 'a']
['z', 'a']
['a']
Yukarıdaki çıktının ilk satırında gördüğünüz gibi, fonksiyon ilk çağrıldığında listede ‘istihza’ karakter dizisini oluşturan bütün harfler var. Yani fonksiyonumuz ilk çalışmada parametre olarak karakter dizisinin tamamını alıyor. Ancak fonksiyonun her yinelenişinde listedeki harfler birer birer düşüyor. Böylece özyinelemeli fonksiyonumuz parametre olarak karakter dizisinin her defasında bir eksiltilmiş biçimini alıyor.
Yukarıdaki sözünü ettiğimiz düşmenin yönü karakter dizisinin başından sonuna doğru. Yani her defasında, elde kalan karakter dizisinin ilk harfi düşüyor. Düşme yönünün böyle olması bizim kodları yazış şeklimizden kaynaklanıyor. Eğer bu kodları şöyle yazsaydık:
def azalt(s):
if len(s) < 1:
return s
else:
print(list(s))
return azalt(s[:-1])
Harflerin düşme yönü sondan başa doğru olacaktı:
['i', 's', 't', 'i', 'h', 'z', 'a']
['i', 's', 't', 'i', 'h', 'z']
['i', 's', 't', 'i', 'h']
['i', 's', 't', 'i']
['i', 's', 't']
['i', 's']
['i']
Burada, bir önceki koddaki azalt(s[1:])
satırını azalt(s[:-1])
şeklinde
değiştirdiğimize dikkat edin.
Fonksiyonun nasıl işlediğini daha iyi anlamak için, ‘istihza’ karakter dizisinin son harfinin her yineleniş esnasındaki konumunun nasıl değiştiğini de izleyebilirsiniz:
n = 0
def azalt(s):
global n
mesaj = '{} harfinin {}. çalışmadaki konumu: {}'
if len(s) < 1:
return s
else:
n += 1
print(mesaj.format('a', n, s.index('a')))
return azalt(s[1:])
azalt('istihza')
Bu kodlar şu çıktıyı verir:
a harfinin 1. çalışmadaki konumu: 6
a harfinin 2. çalışmadaki konumu: 5
a harfinin 3. çalışmadaki konumu: 4
a harfinin 4. çalışmadaki konumu: 3
a harfinin 5. çalışmadaki konumu: 2
a harfinin 6. çalışmadaki konumu: 1
a harfinin 7. çalışmadaki konumu: 0
Gördüğünüz gibi ‘istihza’ kelimesinin en sonunda bulunan ‘a’ harfi her defasında baş tarafa doğru ilerliyor.
Aynı şekilde, kodları daha iyi anlayabilmek için, fonksiyona parametre olarak verdiğimiz ‘istihza’ kelimesinin her yinelemede ne kadar uzunluğa sahip olduğunu da takip edebilirsiniz:
def azalt(s):
if len(s) < 1:
return s
else:
print(len(s))
return azalt(s[:-1])
Bu fonksiyonu ‘istihza’ karakter dizisine uyguladığımızda bize şu çıktıyı veriyor:
7
6
5
4
3
2
1
Gördüğünüz gibi, fonksiyonun kendini her yineleyişinde karakter dizimiz küçülüyor.
Bu durum bize özyinelemeli fonksiyonlar hakkında çok önemli bir bilgi veriyor esasında:
Özyinelemeli fonksiyonlar; büyük bir problemin çözülebilmesi için, o problemin, problemin bütününü temsil eden daha küçük bir parçası üzerinde işlem yapabilmemizi sağlayan fonksiyonlardır.
Yukarıdaki örnekte de bu ilkeyi uyguluyoruz. Yani biz ‘istihza’ karakter dizisinin öncelikle yalnızca ilk karakterini düşürüyoruz:
s[1:]
Daha sonra da bu yöntemi özyinelemeli bir şekilde uyguladığımızda, ‘istihza’ karakter dizisinin her defasında daha küçük bir parçası bu yöntemden etkileniyor:
azalt(s[1:])
Yani fonksiyonumuz ilk olarak ‘istihza’ karakter dizisinin ilk harfi olan ‘i’ harfini düşürüyor. Sonra ‘stihza’ kelimesinin ilk harfi olan ‘s’ harfini düşürüyor. Ardından ‘tihza’ kelimesinin ilk harfi olan ‘t’ harfini düşürüyor ve kelime tükenene kadar bu işlemi devam ettiriyor.
Peki ama bunu nasıl yapıyor?
Şimdi yukarıdaki fonksiyondaki şu kısma dikkatlice bakın:
if len(s) < 1:
return s
İşte burada özyinelemeli fonksiyonumuzun, karakter dizisi üzerinde ne kadar derine inmesi gerektiğini belirliyoruz. Buna göre, karakter dizisinin uzunluğu 1’in altına düştüğünde eldeki karakter dizisini döndürüyoruz. Yani karakter dizisinin uzunluğu 1’in altına düştüğünde elde kalan karakter dizisi boş bir karakter dizisi olduğu için o boş karakter dizisini döndürüyoruz. Eğer istersek elbette bu durumda başka bir şey de döndürebiliriz:
def azalt(s):
if len(s) < 1:
return 'bitti!'
else:
print(s)
return azalt(s[1:])
İşte if len(s) < 1:
bloğunun bulunduğu bu kodlara ‘dip nokta’ adı veriyoruz.
Fonksiyonumuzun yinelene yinelene (veya başka bir ifadeyle ‘dibe ine ine’)
geleceği en son nokta burasıdır. Eğer bu dip noktayı belirtmezsek fonksiyonumuz,
tıpkı dipsiz bir kuyuya düşmüş gibi, sürekli daha derine inmeye çalışacak,
sonunda da hata verecektir. Ne demek istediğimizi daha iyi anlamak için
kodlarımızı şöyle yazalım:
def azalt(s):
print(s)
return azalt(s[1:])
Gördüğünüz gibi burada herhangi bir dip nokta belirtmedik. Bu kodları çalıştırdığımızda Python bize şöyle bir hata mesajı verecek:
RuntimeError: maximum recursion depth exceeded
Yani:
ÇalışmaZamanıHatası: Azami özyineleme derinliği aşıldı
Dediğimiz gibi, özyinelemeli fonksiyonlar her yinelenişte sorunun (yani üzerinde işlem yapılan parametrenin) biraz daha derinine iner. Ancak bu derine inmenin de bir sınırı vardır. Bu sınırın ne olduğunu şu kodlar yardımıyla öğrenebilirsiniz:
>>> import sys
>>> sys.getrecursionlimit()
İşte biz özyinelemeli fonksiyonlarımızda dip noktayı mutlaka belirterek, Python’ın fonksiyonu yinelerken ne kadar derine inip nerede duracağını belirlemiş oluyoruz.
Şimdi son kez, yukarıdaki örnek fonksiyonu, özyineleme mantığını çok daha iyi anlamanızı sağlayacak bir şekilde yeniden yazacağız. Dikkatlice bakın:
def azalt(s):
if len(s) < 1:
return s
else:
print('özyineleme sürecine girerken:', s)
azalt(s[1:])
print('özyineleme sürecinden çıkarken:', s)
azalt('istihza')
Burada, fonksiyon kendini yinelemeye başlamadan hemen önce bir print()
satırı yerleştirerek s değişkeninin durumunu takip ediyoruz:
print('özyineleme sürecine girerken:', s)
Aynı işlemi bir de fonksiyonun kendini yinelemeye başlamasının hemen ardından yapıyoruz:
print('özyineleme sürecinden çıkarken:', s)
Yukarıdaki kodlar bize şu çıktıyı verecek:
özyineleme sürecine girerken: istihza
özyineleme sürecine girerken: stihza
özyineleme sürecine girerken: tihza
özyineleme sürecine girerken: ihza
özyineleme sürecine girerken: hza
özyineleme sürecine girerken: za
özyineleme sürecine girerken: a
özyineleme sürecinden çıkarken: a
özyineleme sürecinden çıkarken: za
özyineleme sürecinden çıkarken: hza
özyineleme sürecinden çıkarken: ihza
özyineleme sürecinden çıkarken: tihza
özyineleme sürecinden çıkarken: stihza
özyineleme sürecinden çıkarken: istihza
Gördüğünüz gibi fonksiyon özyineleme sürecine girerken düşürdüğü her bir karakteri, özyineleme sürecinden çıkarken yeniden döndürüyor. Bu, özyinelemeli fonksiyonların önemli bir özelliğidir. Mesela bu özellikten yararlanarak şöyle bir kod yazabilirsiniz:
def ters_çevir(s):
if len(s) < 1:
return s
else:
ters_çevir(s[1:])
print(s[0])
ters_çevir('istihza')
Yazdığımız bu kodda ters_çevir()
fonksiyonu, kendisine verilen parametreyi
ters çevirecektir. Yani yukarıdaki kod bize şu çıktıyı verir:
a
z
h
i
t
s
i
Burada yaptığımız şey çok basit: Yukarıda da söylediğimiz gibi, özyinelemeli
fonksiyonlar, özyineleme sürecine girerken yaptığı işi, özyineleme sürecinden
çıkarken tersine çevirir. İşte biz de bu özellikten yararlandık. Fonksiyonun
kendini yinelediği noktanın çıkışına bir print()
fonksiyonu yerleştirip,
geri dönen karakterlerin ilk harfini ekrana bastık. Böylece s adlı
parametrenin tersini elde etmiş olduk.
Ancak eğer yukarıdaki kodları bu şekilde yazarsak, fonksiyondan dönen değeri her yerde kullanamayız. Mesela yukarıdaki fonksiyonu aşağıdaki gibi kullanamayız:
def ters_çevir(s):
if len(s) < 1:
return s
else:
ters_çevir(s[1:])
print(s[0])
kelime = input('kelime girin: ')
print('Girdiğiniz kelimenin tersi: {}'.format(ters_çevir(kelime)))
Fonksiyonumuzun daha kullanışlı olabilmesi için kodlarımızı şöyle yazabiliriz:
def ters_çevir(s):
if len(s) < 1:
return s
else:
return ters_çevir(s[1:]) + s[0]
kelime = input('kelime girin: ')
print('Girdiğiniz kelimenin tersi: {}'.format(ters_çevir(kelime)))
Burada bizim amacımızı gerçekleştirmemizi sağlayan satır şu:
return ters_çevir(s[1:]) + s[0]
İlk bakışta bu satırın nasıl çalıştığını anlamak zor gelebilir. Ama aslında son
derece basit bir mantığı var bu kodların. Şöyle düşünün: ters_çevir()
fonksiyonunu özyinelemeli olarak işlettiğimizde, yani şu kodu yazdığımızda:
return ters_çevir(s[1:])
…döndürülecek son değer boş bir karakter dizisidir. İşte biz özyinelemeden çıkılırken geri dönen karakterlerin ilk harflerini bu boş karakter dizisine ekliyoruz ve böylece girdiğimiz karakter dizisinin ters halini elde etmiş oluyoruz.
Yukarıdaki işlevin aynısını, özyinelemeli fonksiyonunuzu şöyle yazarak da elde edebilirdiniz:
def ters_çevir(s):
if not s:
return s
else:
return s[-1] + ters_çevir(s[:-1])
print(ters_çevir('istihza'))
Burada aynı iş için farklı bir yaklaşım benimsedik. İlk olarak, dip noktasını şu şekilde belirledik:
if not s:
return s
Bildiğiniz gibi, boş veri tiplerinin bool değeri False
’tur. Dolayısıyla
özyineleme sırasında s parametresinin uzunluğunun 1’in altına düşmesi, s
parametresinin içinin boşaldığını gösterir. Yani o anda s parametresinin bool
değeri False
olur. Biz de yukarıda bu durumdan faydalandık.
Bir önceki kodlara göre bir başka farklılık da şu satırda:
return s[-1] + ters_çevir(s[:-1])
Burada benimsediğimiz yaklaşımın özü şu: Bildiğiniz gibi bir karakter dizisini ters çevirmek istediğimizde öncelikle bu karakter dizisinin en son karakterini alıp en başa yerleştiririz. Yani mesela elimizdeki karakter dizisi ‘istihza’ ise, bu karakter dizisini ters çevirmenin ilk adımı bunun en son karakteri olan ‘a’ harfini alıp en başa koymaktır. Daha sonra da geri kalan harfleri tek tek tersten buna ekleriz:
düz: istihza
ters: a + z + h + i + t + s + i
İşte yukarıdaki fonksiyonda da yaptığımız şey tam anlamıyla budur.
Önce karakter dizisinin son harfini en başa koyuyoruz:
return s[-1]
Ardından da buna geri kalan harfleri tek tek tersten ekliyoruz:
return s[-1] + ters_çevir(s[:-1])
Özyinelemeli fonksiyonlara ilişkin olarak yukarıda tek bir örnek üzerinde epey açıklama yaptık. Bu örnek ve açıklamalar, özyinelemeli fonksiyonların nasıl çalıştığı konusunda size epey fikir vermiş olmalı. Ancak elbette bu fonksiyonları tek bir örnek yardımıyla tamamen anlayamamış olabilirsiniz. O yüzden gelin isterseniz bir örnek daha verelim. Mesela bu kez de basit bir sayaç yapalım:
def sayaç(sayı, sınır):
print(sayı)
if sayı == sınır:
return 'bitti!'
else:
return sayaç(sayı+1, sınır)
Not
Bu fonksiyonun yaptığı işi elbette başka şekillerde çok daha kolay bir şekilde halledebilirdik. Bu örneği burada vermemizin amacı yalnızca özyinelemeli fonksiyonların nasıl işlediğini göstermek. Yoksa böyle bir işi özyinelemeli fonksiyonlarla yapmanızı beklemiyoruz.
Yukarıdaki fonksiyona dikkatlice bakarsanız aslında yaptığı işi çok basit bir şekilde gerçekleştirdiğini göreceksiniz.
Burada öncelikle sayaç()
adlı bir fonksiyon tanımladık. Bu fonksiyon toplam
iki farklı parametre alıyor: sayı ve sınır.
Buna göre fonksiyonumuzu şöyle kullanıyoruz:
print(sayaç(0, 100))
Burada sayı parametresine verdiğimiz 0 değeri sayacımızın saymaya kaçtan başlayacağını gösteriyor. sınır parametresine verdiğimiz 100 değeri ise kaça kadar sayılacağını gösteriyor. Buna göre biz 0’dan 100’e kadar olan sayıları sayıyoruz…
Gelin şimdi biraz fonksiyonumuzu inceleyelim.
İlk olarak şu satırı görüyoruz fonksiyon gövdesinde:
print(sayı)
Bu satır, özyinelemeli fonksiyonun her yinelenişinde sayı parametresinin durumunu ekrana basacak.
Sonraki iki satırda ise şu kodları görüyoruz:
if sayı == sınır:
return 'bitti!'
Bu bizim ‘dip nokta’ adını verdiğimiz şey. Fonksiyonumuz yalnızca bu noktaya kadar yineleyecek, bu noktanın ilerisine geçmeyecektir. Yani sayı parametresinin değeri sınır parametresinin değerine ulaştığında özyineleme işlemi de sona erecek. Eğer böyle bir dip nokta belirtmezsek fonksiyonumuz sonsuza kadar kendini yinelemeye çalışacak, daha önce sözünü ettiğimiz ‘özyineleme limiti’ nedeniyle de belli bir aşamadan sonra hata verip çökecektir.
Sonraki satırlarda ise şu kodları görüyoruz:
else:
return sayaç(sayı+1, sınır)
Bu satırlar, bir önceki aşamada belirttiğimiz dip noktaya ulaşılana kadar fonksiyonumuzun hangi işlemleri yapacağını gösteriyor. Buna göre, fonksiyonun her yinelenişinde sayı parametresinin değerini 1 sayı artırıyoruz.
Fonksiyonumuzu sayaç(0, 100)
gibi bir komutla çalıştırdığımızı düşünürsek,
fonksiyonun ilk çalışmasında 0 olan sayı değeri sonraki yinelemede 1,
sonraki yinelemede 2, sonraki yinelemede ise 3 olacak ve bu durum sınır
değer olan 100’e varılana kadar devam edecektir. sayı parametresinin değeri
100 olduğunda ise dip nokta olarak verdiğimiz ölçüt devreye girecek ve
fonksiyonun kendi kendisini yinelemesi işlemine son verilecektir.
Biz yukarıdaki örnekte yukarıya doğru sayan bir fonksiyon yazdık. Eğer yukarıdan aşağıya doğru sayan bir sayaç yapmak isterseniz yukarıdaki fonksiyonu şu şekle getirebilirsiniz:
def sayaç(sayı, sınır):
print(sayı)
if sayı == sınır:
return 'bitti!'
else:
return sayaç(sayı-1, sınır)
print(sayaç(100, 0))
Burada, önceki fonksiyonda + olan işleci - işlecine çevirdik:
return sayaç(sayı-1, sınır)
Fonksiyonumuzu çağırırken de elbette sayı parametresinin değerini 100 olarak, sınır parametresinin değerini ise 0 olarak belirledik.
Bu arada, daha önce de bahsettiğimiz gibi, özyinelemeli fonksiyonlar, özyinelemeye başlarken döndürdükleri değeri, özyineleme işleminin sonunda tek tek geri döndürür. Bu özelliği göz önünde bulundurarak yukarıdaki fonksiyonu şu şekilde de yazabilirdiniz:
def sayaç(sayı, sınır):
if sayı == sınır:
return 'bitti!'
else:
sayaç(sayı+1, sınır)
print(sayı)
print(sayaç(0, 10))
Dikkat ederseniz burada print(sayı)
satırını özyineleme işlevinin çıkışına
yerleştirdik. Böylece 0’dan 10’a kadar olan sayıları tersten elde ettik.
Ancak tabii ki yukarıdaki anlamlı bir kod yazım tarzı değil. Çünkü
fonksiyonumuzun yazım tarzıyla yaptığı iş birbiriyle çok ilgisiz. Sayıları
yukarı doğru saymak üzere tasarlandığı belli olan bu kodlar, yalnızca bir
print()
fonksiyonunun özyineleme çıkışına yerleştirilmesi sayesinde yaptığı
işi yapıyor…
Yukarıda verdiğimiz örnekler sayesinde artık özyinelemeli fonksiyonlar hakkında en azından fikir sahibi olduğumuzu söyleyebiliriz. Gelin isterseniz şimdi özyinelemeli fonksiyonlarla ilgili (biraz daha mantıklı) bir örnek vererek bu çetrefilli konuyu zihnimizde netleştirmeye çalışalım.
Bu defaki örneğimizde iç içe geçmiş listeleri tek katmanlı bir liste haline getireceğiz. Yani elimizde şöyle bir liste olduğunu varsayarsak:
l = [1, 2, 3, [4, 5, 6], [7, 8, 9, [10, 11], 12], 13, 14]
Yazacağımız kodlar bu listeyi şu hale getirecek:
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
Bu amacı gerçekleştirebilmek için şöyle bir fonksiyon yazalım:
def düz_liste_yap(liste):
if not isinstance(liste, list):
return [liste]
elif not liste:
return []
else:
return düz_liste_yap(liste[0]) + düz_liste_yap(liste[1:])
l = [1, 2, 3, [4, 5, 6], [7, 8, 9, [10, 11], 12], 13, 14]
print(düz_liste_yap(l))
Bu fonksiyonu yukarıdaki iç içe geçmiş listeye uyguladığınızda istediğiniz sonucu aldığınızı göreceksiniz.
İlk bakışta yukarıdaki kodları anlamak biraz zor gelmiş olabilir. Ama endişe etmenize gerek yok. Zira biz bu kodları olabildiğince ayrıntılı bir şekilde açıklayacağız.
İlk olarak dip noktamızı tanımlıyoruz her zamanki gibi:
if not isinstance(liste, list):
return [liste]
Fonksiyonumuzun temel çalışma prensibine göre liste içindeki bütün öğeleri tek
tek alıp başka bir liste içinde toplayacağız. Eğer liste elemanları üzerinde
ilerlerken karşımıza liste olmayan bir eleman çıkarsa bu elemanı [liste]
koduyla bir listeye dönüştüreceğiz.
Önceki örneklerden farklı olarak, bu kez kodlarımızda iki farklı dip noktası kontrolü görüyoruz. İlkini yukarıda açıkladık. İkinci dip noktamız şu:
elif not liste:
return []
Burada yaptığımız şey şu: Eğer özyineleme esnasında boş bir liste ile karşılaşırsak, tekrar boş bir liste döndürüyoruz. Peki ama neden?
Bildiğiniz gibi boş bir listenin 0. elemanı olmaz. Yani boş bir liste üzerinde şu işlemi yapamayız:
>>> a = []
>>> a[0]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
Gördüğünüz gibi, boş bir liste üzerinde indeksleme işlemi yapmaya kalkıştığımızda hata alıyoruz. Şimdi durumu daha iyi anlayabilmek için isterseniz yukarıdaki kodları bir de ikinci dip noktası kontrolü olmadan yazmayı deneyelim:
def düz_liste_yap(liste):
if not isinstance(liste, list):
return [liste]
else:
return düz_liste_yap(liste[0]) + düz_liste_yap(liste[1:])
l = [1, 2, 3, [4, 5, 6], [7, 8, 9, [10, 11], 12], 13, 14]
print(düz_liste_yap(l))
Bu kodları çalıştırdığımızda şu hata mesajıyla karşılaşıyoruz:
Traceback (most recent call last):
File "deneme.py", line 9, in <module>
print(düz_liste_yap(l))
File "deneme.py", line 5, in düz_liste_yap
return düz_liste_yap(liste[0]) + düz_liste_yap(liste[1:])
File "deneme.py", line 5, in düz_liste_yap
return düz_liste_yap(liste[0]) + düz_liste_yap(liste[1:])
File "deneme.py", line 5, in düz_liste_yap
return düz_liste_yap(liste[0]) + düz_liste_yap(liste[1:])
File "deneme.py", line 5, in düz_liste_yap
return düz_liste_yap(liste[0]) + düz_liste_yap(liste[1:])
File "deneme.py", line 5, in düz_liste_yap
return düz_liste_yap(liste[0]) + düz_liste_yap(liste[1:])
File "deneme.py", line 5, in düz_liste_yap
return düz_liste_yap(liste[0]) + düz_liste_yap(liste[1:])
File "deneme.py", line 5, in düz_liste_yap
return düz_liste_yap(liste[0]) + düz_liste_yap(liste[1:])
File "deneme.py", line 5, in düz_liste_yap
return düz_liste_yap(liste[0]) + düz_liste_yap(liste[1:])
IndexError: list index out of range
Gördüğünüz gibi, biraz önce boş bir liste üzerinde indeksleme yapmaya
çalıştığımızda aldığımız hatanın aynısı bu. Çünkü kodlarımızın else
bloğuna
bakarsanız liste üzerinde indeksleme yaptığımızı görürsünüz:
return düz_liste_yap(liste[0]) + düz_liste_yap(liste[1:])
Elbette boş bir liste liste[0]
veya liste[1:]
gibi sorgulamalara
IndexError
tipinde bir hata mesajıyla cevap verecektir. İşte böyle bir
durumda hata almamak için şu kodları yazıyoruz:
elif not liste:
return []
Böylece özyineleme esnasında boş bir listeyle karşılaştığımızda bu listeyi şu şekle dönüştürüyoruz:
[[]]
Böyle bir yapı üzerinde indeksleme yapılabilir:
>>> a = [[]]
>>> a[0]
[]
Dip noktaya ulaşılana kadar yapılacak işlemler ise şunlar:
return düz_liste_yap(liste[0]) + düz_liste_yap(liste[1:])
Yani listenin ilk öğesine, geri kalan öğeleri teker teker ekliyoruz.
Gelin bir örnek daha verelim:
def topla(sayilar):
if len(sayilar) < 1:
return 0
else:
ilk, son = sayilar[0], sayilar[1:]
return ilk+topla(son)
Bu fonksiyonun görevi, kendisine liste olarak verilen sayıları birbiriyle toplamak. Biz bu işi başka yöntemlerle de yapabileceğimizi biliyoruz, ama bizim burada amacımız özyinelemeli fonksiyonları anlamak. O yüzden sayıları birbiriyle toplama işlemini bir de bu şekilde yapmaya çalışacağız.
Elimizde şöyle bir liste olduğunu varsayalım:
liste = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Böyle bir durumda fonksiyonumuz 55 çıktısı verir.
Gelelim bu fonksiyonu açıklamaya…
Her zamanki gibi ilk olarak dip noktamızı tanımlıyoruz:
if len(sayilar) < 1:
return 0
Buna göre sayilar adlı listenin uzunluğu 1’in altına düşünce 0 değerini döndürüyoruz. Burada 0 değerini döndürmemizin nedeni, listede öğe kalmadığında programımızın hata vermesini önlemek. Eğer 0 dışında başka bir sayı döndürürsek bu sayı toplama işleminin sonucuna etki edecektir. Toplama işleminin sonucunu etkilemeyecek tek sayı 0 olduğu için biz de bu sayıyı döndürüyoruz.
Taban noktaya varılıncaya kadar yapılacak işlemler ise şunlar:
ilk, son = sayilar[0], sayilar[1:]
return ilk+topla(son)
Burada amacımız, listenin ilk sayısı ile listenin geri kalan öğelerini tek tek birbiriyle toplamak. Bunun için sayilar adlı listenin ilk öğesini, listenin geri kalanından ayırıyoruz ve ilk öğeyi ilk; geri kalan öğeleri ise son adlı bir değişkene gönderiyoruz:
ilk, son = sayilar[0], sayilar[1:]
Sonra da ilk öğeyi, geri kalan liste öğeleri ile tek tek topluyoruz. Bunun için
de topla()
fonksiyonunun kendisini son adlı değişken içinde tutulan liste
öğelerine özyinelemeli olarak uyguluyoruz:
return ilk+topla(son)
Böylece liste içindeki bütün öğelerin toplam değerini elde etmiş oluyoruz.
Bu arada, yeri gelmişken Python programlama dilinin pratik bir özelliğinden söz edelim. Gördüğünüz gibi sayıların ilk öğesini geri kalan öğelerden ayırmak için şöyle bir kod yazdık:
ilk, son = sayilar[0], sayilar[1:]
Aslında aynı işi çok daha pratik bir şekilde de halledebilirdik. Dikkatlice bakın:
ilk, *son = sayilar
Böylece sayilar değişkenin ilk öğesi ilk değişkeninde, geri kalan öğeleri ise son değişkeninde tutulacaktır. İlerleyen derslerde ‘Yürüyücüler’ (Iterators) konusunu işlerken bu yapıdan daha ayrıntılı bir şekilde söz edeceğiz.
İç İçe (Nested) Fonksiyonlar¶
Bu bölümde iç içe fonksiyonların ne olduklarını ve nasıl kullanılabileceklerini inceleceğiz.
İç İçe Fonksiyonlar Nedir?¶
İsminden anlayabileceğimiz gibi içe içe olan birden fazla fonksiyonumuz olunca bunlara nested, yani iç içe fonksiyonlar diyoruz. Aşağıdaki gibi iki fonksiyonumuz olduğunu düşünelim:
def fonk1():
def fonk2():
...
Burada fonk1
kapsayıcı (enclosing) veya dış fonksiyonumuz, fonk2
ise içerideki (nested) yani iç
fonksiyonumuz oluyor. İç içe fonksiyonlarımızın ilginç özellikleri olduğunu
söyleyebiliriz. Ayrıca bu fonksiyonları iyice anlamak, ileride üreteçleri
(diğer bir adı ile yürüyücüleri) de daha iyi anlamamızı sağlayacaktır.
İç içe fonksiyonları anlamanın en iyi yolu örnek üzerinden gitmektir. Şimdi şöyle bir fonksiyon tanımlayalım:
def yazıcı():
def yaz(mesaj):
print(mesaj)
return yaz
Kodu çalıştırıp etkileşimli kabuğu kullanalım:
>>> y = yazıcı()
>>> y("Merhaba")
Merhaba
>>> type(y)
<class 'function'>
>>> y
<function yazıcı.<locals>.yaz at 0x00000210D9235558>
Şimdi bu çıktılarımızı inceleyelim. yazıcı
fonksiyonumuz çağrıldığında değer olarak
yaz
fonksiyonunu çeviriyor. Bu yaz
fonksiyonu da yazıcı
fonksiyonumuzun
içerisinde tanımladığı için bizim iç fonksiyonumuz oluyor. yazıcı
ise
kapsayıcı fonksiyonumuz. y("Merhaba")
komutu çağırıldığında ekrana Merhaba
yazılıyor. Çünkü y
’ye atanan değer olan yaz
fonksiyonunun yaptığı iş
buydu. Dikkat ederseniz y
’nin türünün de function
olduğunu görebilirsiniz.
Son çıktımızda ise alışılmışın dışında bir <locals>
ifadesi görüyoruz.
Şimdi biraz bunun üzerine konuşacağız.
Normalde bir fonksiyon yazdığımızda ve bu fonksiyon başka bir fonksiyonun içerisinde olmadığında, programı çalıştırıldığımızda ve kod işleme sırası bu fonksiyona geldiğinde fonksiyonumuz tanımlanmış olur. Yani bu fonksiyonun ne olduğu, ne yapacağı artık Python yorumlayıcısı tarafından bilinmektedir. Ayrıca bu fonksiyondan sadece bir tane vardır. Örneğin fonksiyonumuz şu şekilde ise:
def fonk():
pass
Her fonk()
yazdığımızda aynı fonksiyon çağrılır. Dikkat edin, aynı işlemler yapılır
demiyorum. Aynı fonksiyon çağrılır. Yapacağı işlem burada bizim için önemli değil.
Şimdi de iç içe fonksiyon tanımımıza ve şu <locals>
kelimesine bakalım.
İlk önce:
def yazıcı():
def yaz(mesaj):
print(mesaj)
return yaz
Şeklinde kapsayıcı fonksiyonumuzu tanımlamış oluyoruz. Dikkat ederseniz sadece kapsa-
yıcı fonksiyonun tanımlandığını söyledim. Artık yazıcı
fonksiyonunun, Python yorum-
layıcısı tarafından ne yapacağı, nasıl çalışacağı biliniyor. Ancak yaz
fonksiyonu için
aynı şeyleri söyleyemeyiz. Sonuç olarak bir fonksiyon çağırılmadan içerisindeki
komutlar çalışmaz. Eğer def yaz...
komutu çalışmaz ise de yaz
fonksiyonumuz tanımlanmış
olmaz. Yani şu anda yaz
fonksiyonumuz tanımlanmamıştır. Peki ne zaman tanımlanacaktır?
Tabii ki de yazıcı
fonksiyonumuzu çağırdığımız zaman. Çünkü dediğimiz gibi,
yazıcı fonksiyonu çağrılmadığı sürece def yaz...
bölümü çalışmıyor. Python yorumlayıcısı
programımız çalışırken yazıcı
fonksiyonunun ne yapacağını bilir, dolayısı ile de
yaz
fonksiyonununun nasıl tanımlanacağını bilir. Ancak yaz
fonksiyonu
tanımlanmadan önce ne yapacağını bilemez. Buradan önemli
yerlere varacağımız için bu kısmın anlaşılması gerekiyor. Şimdi şunu söyleyebiliriz ki
yazıcı
fonksiyonumuzu her çağırdımızda yaz
sınıfı en baştan tanımlanır.
Bu da yazıcı
fonksiyonumuzu her çağırışımızda yeni tanımlanan yaz
fonksiyonunun
farklı ve tek olduğu anlamına gelir. Yani kapsayıcı olan yazıcı
fonksiyonu
sadece bir tane iken döndürdüğü yaz
fonksiyonu birden fazla ve farklı oluyor.
Yani yazıcı
fonksiyonumuzu her çağırdığımızda sadece o çağırışımıza özel bir
yaz
fonksiyonu elde ediyoruz. İşte bu <locals>
kelimesi buradan geliyor.
Yani:
>>> y
<function yazıcı.<locals>.yaz at 0x00000210D9235558>
Bu demek oluyor ki bizim y
değişkenimiz, daha önceki bir yazıcı
fonksiyonunun çağrısına
ait, yani onun içinde tanımlanan bir yaz
fonksiyonudur. locals
da zaten
yerel değişkenler anlamına gelir. Yani buradaki yaz
fonksiyonu, daha önce çağırdığımız
yazıcı
fonksiyonunun içinde tanımlanan yerel bir değişkendir. Tanımlanan her yaz
fonksiyonunun farklı olduğunu şu şekilde de görebiliriz:
>>> y = yazıcı()
>>> b = yazıcı()
>>> y
<function yazıcı.<locals>.yaz at 0x00000210D9235558>
>>> b
<function yazıcı.<locals>.yaz at 0x00000210D920E678>
>>> id(y)
2271385703768
>>> id(b)
2271385544312
Gördüğünüz gibi farklı yaz
fonksiyonlarının hafızada saklandığı yerler de
farklı oluyor…
Bu konuda biraz daha ilerlemeden önce bilmemiz gereken başka şeyler de var. Biraz da onlar hakkında konuşalım.
‘nonlocal’ Deyimi¶
nonlocal
deyimi yerel olmayan anlamına gelir. Kullanım amacı global
deyimi
ile benzerdir. Ancak bunu kullanmamız küresel yani global değişkenlere ulaşmamızı değil,
yerel olmayan değişkenlere ulaşmamızı sağlar. Ayrıca bu deyimi sadece iç içe fonksiyonlarda kullanabiliriz. Tabii bunu böyle söyleyince bir şey anlaşılmıyor. Örnek vermek lazım:
def kapsayıcı_fonk():
non_local_değişken = 1
def iç_fonk():
non_local_değişken = 2
print(non_local_değişken)
return iç_fonk
Burada iç içe bir fonksiyon yapısına sahibiz. Şimdi bu kodumuzu çalıştırıp etkileşimli kabukta denemeler yapalım:
>>> dönüş_fonksiyonu = kapsayıcı_fonk()
>>> dönüş_fonksiyonu()
2
Gördüğünüz gibi 1
yazılmadı. Yani kapsayıcı fonksiyona ait olan non_local_değişken
ile iç fonksiyonumuza ait olan non_local_değişken
farklılar. Aynı bu örnekte:
a = 1
def fonk():
a = 2
print(a)
>>> fonk()
2
küresel a
değişkeni ile fonk
fonksiyonuna ait a
değişkeninin farklı olması gibi.
Peki biz burada fonksiyon içinde de küresel a
’yı kullanmak istersek nasıl yaparız?
Bir şey yapmamıza gerek yok, zaten fonksiyon kendi içinde a
değişkenini bulamayınca global alana bakacaktır:
a = 1
def fonk():
print(a)
>>> fonk()
1
Fakat eğer küresel olan a değişkenini değiştirmek istiyorsanız bildiğiniz gibi global
deyimini kullanmamız lazım:
a = 1
def fonk():
global a
a += 1
print(a)
>>> fonk()
2
>>> a
2
İşte aynı bunun gibi:
def kapsayıcı_fonk():
non_local_değişken = 1
def iç_fonk():
non_local_değişken = 2
print(non_local_değişken)
return iç_fonk
Örneğimizde de iç_fonk
’un içinde kapsayıcı_fonk
’a ait olan non_local_değişken
değişkenini değiştirmek istersek bunu da nonlocal
deyimi ile şöyle yapabiliriz:
def kapsayıcı_fonk():
non_local_değişken = 1
def iç_fonk():
nonlocal non_local_değişken
non_local_değişken += 1
print(non_local_değişken)
return iç_fonk
>>> dönüş_fonksiyonu = kapsayıcı_fonk()
>>> dönüş_fonksiyonu()
2
Tabii bu değişkeni değiştirmek gibi bir amacımız yoksa, sadece kullanmak isteseydik şöyle de yapabilirdik ve nonlocal
deyimine gerek kalmazdı:
def kapsayıcı_fonk():
non_local_değişken = 1
def iç_fonk():
print(non_local_değişken)
return iç_fonk
>>> dönüş_fonksiyonu = kapsayıcı_fonk()
>>> dönüş_fonksiyonu()
1
Gördüğünüz gibi nonlocal
ifadesi iç içe fonksiyonlar ile çalışırken iç fonksiyonda,
kapsayıcı fonksiyonunun değişkenlerini değiştirmemizi sağlıyor. Artık bu bilgiyi kullanarak
şöyle bir fonksiyon oluşturabiliriz:
def yazıcı(mesaj):
def yaz():
nonlocal mesaj
mesaj += " Dünya"
print(mesaj)
return yaz
>>> y = yazıcı("Merhaba")
>>> y()
Merhaba Dünya
nonlocal
deyiminin nasıl kullanıldığını bildiğiniz için örneğimizi anladığınızı
düşünüyorum. Burda yaptığımız tek farklı şey nonlocal
deyimi ile birlikte
kullandığımız nesnenin yazıcı
fonksiyonunun parametresi olması. Bunu yapmamızda
bir sakınca yoktur. Sonuç olarak mesaj
parametresi, normalde de yazıcı
fonksiyonu
içerisinde bir değişken gibi kullanılmaktadır. Ancak şunu da unutmayalım ki aynı
global
ifadesini kullanırken olduğu gibi nonlocal
ifadesinde de eğer
daha üst bir alandaki değişkenin üzerinde bir değer atama işleci kullanmayacaksak
nonlocal
ifadesini kullanmamıza gerek yoktur. Yani değişkeni nonlocal
ifadesi
olmadan da kullanabiliriz, ancak değerini değiştiremeyiz. Eğer yukarıdaki kodda nonlocal
ifadesini kullanmazsak hata alırız:
def yazıcı(mesaj):
def yaz():
mesaj += " Dünya"
print(mesaj)
return yaz
>>> y = yazıcı("Merhaba Dünya")
>>> y()
Traceback (most recent call last):
File "<pyshell#1>", line 1, in <module>
y()
File "C:\Users\Dinçel\Desktop\istihza.py", line 3, in yaz
mesaj += " Dünya"
UnboundLocalError: local variable 'mesaj' referenced before assignment
Sonuç olarak kapsayıcı fonksiyona ait değişkenleri, iç fonksiyonumuzda değiştirebilmek
için nonlocal
ifadesine ihtiyacımız vardır.
Şimdi en başta konuştuğumuz <locals>
konusuna geri dönüyoruz. İç fonksiyonun,
çağırılan kapsayıcı fonksiyonun yerel değişkenlerinden biri olduğunu ve
her seferinde yeniden tanımlandığını, bu yüzden de aynı işi yapsalar da
aslında farklı olan fonksiyonlar elde ettiğimizi konuşmuştuk. Ancak her seferinde
yeniden tanımlanan tek şey iç fonksiyon değildir. Kapsayıcı fonksiyonun içindeki
her değişken, dış fonksiyonun her çağırılışında baştan tanımlanır.
Bunu şu örnek üzerinden anlamaya çalışalım:
def sayıcı():
sayı = 0
def say():
nonlocal sayı
sayı += 1
return sayı
return say
Kodumuzu kısaca incelersek say
fonksiyonunda sayı
değişkenini nonlocal
hale getiriyoruz. Aynı zamanda say
fonksiyonu her çağırıldığında sayı
değiş-
kenini de bir artırıp değer olarak döndürüyoruz. Şimdi kodumuzu çalıştıralım:
>>> s = sayıcı()
>>> type(s)
<class 'function'>
>>> s
<function sayıcı.<locals>.say at 0x000001FD2213ED38>
>>>
>>> s()
1
>>> s()
2
>>> s()
3
>>> s()
4
Gördüğünüz gibi ilginç bir şekilde sayıcı
fonksiyonu çalışmış ve bitmiştir,
ancak içerisinde bulunan sayı
değişkeni silinmemiştir ve geri döndürülen
say
fonksiyonu tarafından kullanılmaya devam etmektedir. Yani biz göremesek de
sayı
değişkeni hala bir yerlede saklanılıyordur. Peki normalde bir fonksiyonun
çalışması bitince yerel değişkenleri silinmez mi? Tabii ki silinir. Ancak burada say
fonksiyonu içinde sayı
değişkenini nonlocal
hale getirmiş oluyoruz. Yani aslında
biz sayı
değişkenini kullanmaya devam ediyoruz. Eee şimdi Python kalkıp da bizim
kullanacağımız bir değişkeni silse ayıp olur. O da bunu yapmıyor zaten. Ancak
sayı
değişkeni iç fonksiyon olan say
fonksiyonunda hiç kullanılmasaydı silinirdi. Aslında bu
örnekteki kilit olaylardan biri de sayı
değişkeninin sadece bir defa tanımlanması
ve bu tanımın aynı say
fonksiyonunda olduğu gibi sayıcı
fonksiyonumuzun sadece bir çağırılışına özgü olması. Buradan iki sonuca varıyoruz:
sayıcı
sınıfını birden fazla defa çağırsak bile geri döndürülen hersay
fonksiyonu ekrana sayıları hep sırayla yazdıracaktır. Çünkü hersay
fonksiyonu kendisini tanımlayansayıcı
çağırılışına ait olansayı
değişkenini kullanmaktadır.Her
say
fonksiyonunun kullandığısayı
değişkeni sadece bir defa0
olarak tanımlanmakta ve daha sonrasay
fonksiyonumuzu her çağırışımızda artmaktadır.
Evet dediğimiz gibi farklı say
fonksiyonları farklı sayı
değişkenlerini kullanıyor:
>>> s = sayıcı()
>>> s()
1
>>> s()
2
>>> s()
3
>>> s()
4
>>>
>>> s2 = sayıcı()
>>> s2()
1
>>> s2()
2
>>> s2()
3
>>> s2()
4
Eğer bu örnekleri anlamakta zorluk çektiyseniz bunun çalışma mantığı olarak şunun ile aynı olduğunu söyleyebiliriz:
sayı = 0
def say():
global sayı
sayı += 1
print(sayı)
>>> s = say
>>> s()
1
>>> s()
2
>>> s()
3
>>> s()
4
global
deyimi ile yaptığımız bu örneğin nonlocal
ile yaptığımız örnekten belki de en önemli
farkı, nonlocal
örneğinde sayı
değişkenine doğrudan erişememizdir. Ama sayı
değişkenini say
fonksiyonu tarafından kullanılmaktadır. Ancak bizim sayı
değişkenine bizzat erişememiz, gördüğümüz gibi, silindiği anlamına gelmiyor…
İç İçe Fonksiyonların Kullanım Alanları¶
Şu ana kadar iç içe fonksiyonların nasıl tanımlandığını ve nasıl çalıştığını öğrendik. Ancak öğrenme aşamasında olduğumuz için buraya kadar hep basit örnekler verdik. Şimdi bazı işe yarar örnekler vereceğiz ve ne zaman içe içe fonksiyon kullanıp ne zaman normal fonksiyonlar kullanmamızın daha doğru olacağını konuşacağız.
Öncelikle şunu söyleyelim ki iç içe fonksiyonların en fazla kullanıldığı yer bezeyicilerdir. Bu daha sonra göreceğimiz bir konu ancak orada iç içe fonksiyonları çok fazla kullanacağız, haberiniz olsun.
İç içe fonksiyonlar bazı işlemleri daha verimli yapmamızı sağlayabileceği gibi bazı işlemleri de (yanlış veya gereksiz yere kullanırsak) yavaşlatırlar. Mesela şu fonksiyona bakalım:
def işlem_yap(sayı, bölen, *eklenenler):
sonuç = sayı / bölen
for i in eklenenler:
sonuç += i
return sonuç
Bu fonksiyonumuz aldığı sayı
parametresini bölen
parametresi ile böldükten sonra geriye kalan bütün parametreleri sonuca ekleyip geri döndürüyor. *eklenenler
’in ne anlama geldiğini zaten daha önce öğrenmiştik. şimdi bu fonksiyonu kullanalım:
>>> işlem_yap(10, 2, 5, 7)
17.0
>>> işlem_yap(8, 4, 1, 3)
6.0
Şimdi diyelim ki biz yazdığımız programda farklı sayı
ve bölen
parametreleri ile hep aynı eklenenler
parametrelerini kullanacağız. Yani şunun gibi işlemler yapacağız:
>>> işlem_yap(4, 2, 1, 4, 5)
12.0
>>> işlem_yap(60, 12, 1, 4, 5)
15.0
>>> işlem_yap(48, 4, 1, 4, 5)
22.0
>>> işlem_yap(12, 6, 3, 6, 2)
13.0
>>> işlem_yap(12, 4, 3, 6, 2)
14.0
>>> işlem_yap(105, 15, 3, 6, 2)
18.0
Burada görebileceğimiz gibi aynı eklenenler
değerleri çoklukla kullanılıyor. Böyle bir durumda toplama işlemini her seferinde gerçekleştirmemiz gereksiz oluyor. Bu işlemin sadece bir defa yapılmasını şu şekilde sağlayabiliriz:
def işlem_yapıcı(*eklenenler):
ekle = 0
for i in eklenenler:
ekle += i
def işlem(sayı, bölen):
return sayı/bölen + ekle
return işlem
Bu kodumuzda işlem_yapıcı
fonksiyonu hep aynı olacağı için değişmeyecek olan eklenenler
parametresini sadece bir defa alıyor ve hepsini topluyor, daha sonra işlem
fonksiyonunu geri döndürüyor. işlem
fonksiyonunu çağırdığımızda da sayı
ve bölen
parametrelerini veriyoruz ve işlemin sonucu bize geri dönüyor. İlk yaptığımız işlemleri bir de böyle kullanalım:
>>> işlemci = işlem_yapıcı(1, 4, 5)
>>> işlemci2 = işlem_yapıcı(3, 6, 2)
>>> işlemci(4, 2)
12.0
>>> işlemci(60, 12)
15.0
>>> işlemci(48, 4)
22.0
>>> işlemci2(12, 6)
13.0
>>> işlemci2(12, 4)
14.0
>>> işlemci2(105, 15)
18.0
Artık gerekli işlemi yapacak fonksiyonu sadece bir defa oluşturuyoruz ve sürekli onu kullanıyoruz. Bu da aynı parametrelerin sürekli fonksiyona parametre olarak yollanmasını engelliyor ve gerekli işlemlerin sadece bir defa yapılmasını sağlıyor. Kendi yazdığınız kodlarda herhangi bir amaç ile bir fonksiyon oluşturduğunuzda ve bu fonksiyonu da kullanırken bunun gibi bir durum ile karşılaştırdığımızda artık iç içe fonksiyonları kullanarak kodu nasıl daha verimli hale getireceğiniz hakkında aklınızda bir fikir oluşmuştur diye düşünüyorum.
Şimdi de bir fonksiyon oluştururken o fonksiyonun içinde kod tekrarları yaptığımız fark ettiğimizi varsayalım. Böyle bir durumda bu kod tekrarlarını da azaltmak için bir fonksiyon daha yazmamız iyi olacaktır. Yani:
def dosyadaki_karakter_sayısı(dosya, karakter):
sonuç = 0
if type(dosya) == str:
with open(dosya, "r") as f:
veri = f.read()
for i in veri:
if i == karakter:
sonuç += 1
else:
veri = dosya.read()
for i in veri:
if i == karakter:
sonuç += 1
return sonuç
Elimizde bir dosyayı okuyacak ve bu dosyadaki belli bir karakterin sayını döndürecek bir fonksiyon var. Ama bu fonksiyon dosya
parametresi olarak hem dosyanın ismini hem de açılmış bir dosyanın kendisini alabiliyor. if type(dosya) == str: kısmı dosya
değişkeninin türünün str
olup olmadığını kontrol ediyor, eğer öyleyse dosyayı açıyoruz ve okuyoruz. Öyle değilse dosyayı direkt okuyoruz. Dikkat ederseniz daha sonra yapılan işlemler aynı, yani:
for i in veri:
if i == karakter:
sonuç += 1
kısmı iki defa tekrar ediyor. Hatırlarsanız bir karakter dizisinin içinde herhangi bir karakterin kaç defa geçtiğini öğrenmek için count
metodundan faydalanabiliriz:
>>> "merhaba".count("a")
2
Ama burada örneğimiz anlaşılsın diye bunu kendimiz yapıyoruz.
Şimdi yukarıdaki tekrar eden yeri şu şekilde ayrı bir fonksiyon haline getirebiliriz:
def karakter_sayısı(karakter_dizisi, karakter):
sayaç = 0
for i in karakter_dizisi:
if i == karakter:
sayaç += 1
return sayaç
def dosyadaki_karakter_sayısı(dosya, karakter):
if type(dosya) == str:
with open(dosya, "r") as f:
return karakter_sayısı(f.read(), karakter)
else:
return karakter_sayısı(dosya.read(), karakter)
Artık karakter dizisinin içinde bir karakterin kaç defa geçtiğini bulmak için karakter_sayısı
sayısı adlı fonksiyon yararlanıyoruz. Ancak bizim bu fonksiyonu tanımlama sebebimiz dosyadaki_karakter_sayısı fonksiyonunda yaptığımız bir işlemi yerine getirmekdi. Eğer karakter_sayısı
fonksiyonunu programımızda sadece dosyadaki_karakter_sayısı
fonksiyonu içinde kullanacaksak bu fonksiyonu global alanda tanımlamamıza gerek yokü, dosyadaki_karakter_sayısı fonksiyonunun içinde de tanımlayabiliriz:
def dosyadaki_karakter_sayısı(dosya, karakter):
def karakter_sayısı(karakter_dizisi):
sayaç = 0
for i in karakter_dizisi:
if i == karakter:
sayaç += 1
return sayaç
if type(dosya) == str:
with open(dosya, "r") as f:
return karakter_sayısı(f.read())
else:
return karakter_sayısı(dosya.read())
Ayrıca bu şekilde karakter_sayısı
fonksiyonunun karakter
şeklinde bir parametreye ihtiyacı kalmadı, zaten
dosyadaki_karakter_sayısı fonksiyonunun içindeki karakter
değişkenine erişebiliyor. İç içe fonksiyonları bunun gibi durumlarda da kullanabiliriz.
Üreteçler (Generators)¶
Biz üreteçlerle az çok tanışıyoruz. Liste üreteçleri olsun, sözlük üreteçleri olsun bu konu hakkında bir şeyler öğrenmiştik. Ancak biz üreteçlerimizi hep şunun gibi tanımlamıştık:
>>> listem = [i for i in range(10)]
>>> listem
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Dikkat ederseniz burada i for i in range(10)
kısmı (nasıl lambda
fonksiyonlar normal yolla tanımlanan fonksiyonlardan farklı bir söz dizimi kullanıyorsa) normal kodlardan biraz farklı bir söz dizimi kullanıyor. Bu söz dizimi ile karmaşık
algoritmalar oluşturmak zordur, çoğunlukla da mümkün değildir. Zaten bunun bulunma sebebi
karmaşık algoritmalarda kullanılması değil, kısa işlerde yazım kolaylığı sağlamasıdır.
Yani bu yazım şekli, bazı fonksiyonların lambda
olarak tanımlanması gibi,
üreteç tanımlamanın sadece kısa bir yoludur. Peki aslında üreteçler nasıl
tanımlanır? Şimdi gelin bu konuyu inceleyelim.
Üreteçlere Giriş¶
Üreteçler, fonksiyonlara benzer şekilde tanımlanır. Hatta tek farkının yield
adındaki bir ifade olduğunu söyleyebiliriz. Hatırlarsanız iç içe fonksiyonlar
konusunda üreteçler konusuna birkaç defa atıfta bulunmuştuk.
Bu yüzden aynı işi yapacak iç içe bir fonksiyon ile bir üreteci karşılaştırarak
konuya başlamak istiyorum:
def fonksiyon_sayıcı():
sayı = 0
def say():
nonlocal sayı
sayı += 1
return sayı
return say
def üreteç_sayıcı():
sayı = 0
while True:
sayı += 1
yield sayı
Endişe etmeyin. İleride üreteç_sayıcı
’nın nasıl çalıştığını inceleyeceğiz.
Şimdilik sadece şuraya odaklanalım:
>>> type(fonksiyon_sayıcı)
<class 'function'>
>>> type(üreteç_sayıcı)
<class 'function'>
>>> fonk = fonksiyon_sayıcı()
>>> üreteç = üreteç_sayıcı()
>>> type(fonk)
<class 'function'>
>>> type(üreteç)
<class 'generator'>
>>> fonk()
1
>>> fonk()
2
>>> fonk()
3
>>> fonk()
4
>>> next(üreteç)
1
>>> next(üreteç)
2
>>> next(üreteç)
3
>>> next(üreteç)
4
fonk
ve üreteç
değişkenlerini kullanarak elde ettiğimiz sonuçların aynı olduğunu görebiliyorsunuz. Şimdi
bundan faydalanarak tanımlanma şekillerini anlamaya çalışalım.
fonk
fonksiyonunun nasıl çalıştığını zaten iç içe fonksiyonlar konusunda gördük.
Şimdi next
fonksiyonu ve yield
deyimi ile alakalı konuşalım. Öncelikle
şunu söylemek gerekir ki next
fonksiyonu, gömülü bir fonksiyondur. Ne işe yaradığını anlamak
için ise yield
deyimini anlamamız gerekiyor. Eğer kodumuzu ve aldığımız
çıktıları incelerseniz yield
deyiminin, return
deyimine bazı yönlerden
benzediğini fark edebilirsiniz. Tabii önemli farklılıklar da var. Bir kere
fark edeceğiniz gibi yield
deyimi hangi değeri döndüreceğimizi
belirliyor. Peki bu döndürme işleminin return
ile değer döndürmekten
ne farkı var? Bir fonksiyonun içinde return
deyimine ulaşıldığında
fonksiyon sonlanır ve fonksiyona ait yerel değişkenler silinir. yield
deyiminde böyle
bir şey söz konusu değildir. Aynı iç içe fonksiyonlarda iç fonksiyonunun dış fonksiyondaki değişkeni kullanması gibi
üreteçlerin de yerel değişkenleri Python tarafından saklanır. Ancak üreteçlerde
belli değişkenler değil, yerel değişkenlerin tamamı saklanır. Şimdi yukarıdaki örnekte şu üç kısma
tekrar bakarsak:
>>> type(fonksiyon_sayıcı)
<class 'function'>
>>> type(üreteç_sayıcı)
<class 'function'>
>>> fonk = fonksiyon_sayıcı()
>>> üreteç = üreteç_sayıcı()
>>> type(fonk)
<class 'function'>
>>> type(üreteç)
<class 'generator'>
Şunu görüyoruz ki üreteç_sayıcı
aslında bir fonksiyon. Ama alelade bir
fonksiyon değil, çağrıldığında generator
nesnesi döndüren bir fonksiyon.
Yani aynı iç içe fonksiyonlarda önce kapsayıcı fonksiyonu çağırıp dönüş değerini kullandığımız
gibi üreteçlerde de önce üreteci tanımladığımız fonksiyonu çağırıp dönüş değerini
kullanıyoruz. Çünkü aslında üreteç olan nesne, bu döndürülen değerdir. Ve aynı
iç içe fonksiyonlarda olduğu gibi bu durum birbirinden bağımsız ancak aynı işi
yapan değişkenler oluşturmamızı sağlar. Dikkat ederseniz iç içe fonksiyonlar ve
üreteçler, çalışma prensibi açısından benzerler. Ancak üreteçler yield
ifadesininin
kullanımı ile bize daha kullanışlı bir algoritma şekli vermektedir.
Şu ana kadar üreteçlerin nasıl tanımlandığı ve nasıl kullanıldığı hakkında pek de bilgi
vermedik. Yaptığımız şey, iç içe fonksiyonlar ile üreteçlerin, çalışma
prensiblerinin ne kadar benzer olduğuna dikkat çekmek idi. Şimdi next
fonksiyonu ve
yield
deyimi hakkında konuşarak kendi üreteçlerimizi nasıl tanımlayacağımıza
bakalım.
Üreteçlerin Tanımlanması¶
‘yield’ Deyimi ve ‘next’ Fonksiyonu¶
next
fonksiyonunun gömülü bir fonksiyon olduğunu söylemiştik. yield
deyimi da
üretecimizden değer döndürmemizi sağlıyordu. Peki bu işlemler hangi kurallar çerçevesinde
gerçekleşiyor?
Basit bir üreteç tanımlayarak yield
metodunu anlatmaya çalışalım:
def üreteç():
yield "Merhaba"
yield "Dünya"
return
deyiminin fonksiyonu sonlandırırken yield
deyimi üretecin çalışmasına ara
verir ve sağındaki değişkeni geriye döndürür. Herhangi bir değer verilmemiş ise None
döndürecektir.
Şimdi kodumuzu çalıştıralım:
>>> g = üreteç()
>>> next(g)
"Merhaba"
>>> next(g)
"Dünya"
>>> next(g)
Traceback (most recent call last):
File "<pyshell#5>", line 1, in <module>
next(g)
StopIteration
Çıktımızı incelersek next
fonksiyonunun, kendisine verilen üretecin kodunu bir yield
deyimine
rastlayana kadar çalıştırdığını, yield
deyimine rastladığında ise deyimin sağındaki
değişkeni döndürdüğünü görebiliriz. Unutmayalım ki bu döndürme işlemini yapan next
fonksiyonudur.
Üretecimizin içinde herhangi bir yönerge kalmadığında ise next
fonksiyonumuz StopIteration
hatası yükseltmektedir.
Not
‘next’ fonksiyonunun burada yaptığı iş için ‘yineleme (iteration)’ terimi kullanılır. ‘next’ fonksiyonuna parametre olarak verilebilen nesneler ise birer ‘yinelenebilir nesne (iterable object)’dir. ‘generator’ sınıfı yinelenebilir nesnelere bir örnektir.
Bir örnek daha yapalım:
def üreteç():
print("üreteç ilk defa next fonksiyonu ile kullanıldı.")
yield "1. yield"
print("üreteç ikinci defa next fonksiyonu ile kullanıldı.")
yield "2. yield"
print("üreteç üçüncü defa next fonksiyonu ile kullanıldı ve bitti.")
>>> g = üreteç()
>>> ilk_dönüş = next(g)
üreteç ilk defa next fonksiyonu ile kullanıldı.
>>> ikinci_dönüş = next(g)
üreteç ikinci defa next fonksiyonu ile kullanıldı.
>>> son_dönüş = next(g)
üreteç üçüncü defa next fonksiyonu ile kullanıldı ve bitti.
Traceback (most recent call last):
File "<pyshell#5>", line 1, in <module>
next(g)
StopIteration
>>>
>>> ilk_dönüş
'1. yield'
>>> ikinci_dönüş
'2. yield'
>>> son_dönüş
Traceback (most recent call last):
File "<pyshell#0>", line 1, in <module>
son_dönüş
NameError: name 'son_dönüş' is not defined
Örneğimiz gayet açık. next
fonksiyonu kendisine verilen üretecin kodunu en sol kaldığı yerden çalıştırmaya devam ediyor, bir yield
ifadesine denk geldiğinde de üretecin çalışması duruyor ve next
fonksiyonu yield
deyiminin sağındaki değeri geri döndürüyor. Tabii son_dönüş
’ün None
olmak yerine tanımlanmamış olması da ilginç gelmiş olabilir. Bunu da şu örnekle açıklayabiliriz:
>>> def hata():
raise Exception
>>> dönüş = hata()
Traceback (most recent call last):
File "<pyshell#8>", line 1, in <module>
dönüş = hata()
File "<pyshell#7>", line 2, in hata
raise Exception
Exception
>>> dönüş
Traceback (most recent call last):
File "<pyshell#9>", line 1, in <module>
dönüş
NameError: name 'dönüş' is not defined
Gördüğümüz gibi son_dönüş
değişkenimizin tanımlanmamış olmasının sebebi de next
fonksiyonunun değer döndürmek yerine hata yükseltmiş olmasıdır.
Buraya kadar yaptığımız örnekleri iç içe fonksiyonlar ile de kolayca yapabilirdik. Üreteçlerin
önemli bir özelliği de tanımlanırken , fonksiyonlar gibi, her türlü ifade ile kullanılabilmesidir.
Örnek olarak while
döngüsü kullanarak, 1’den başlayarak her yinelediğimizde fibonacci
sayı dizisinin bir sonraki elemanını döndürecek bir üreteç yazalım:
def fibonacci():
x = 1
y = 0
z = 0
while True:
z = y
y = x
x = y + z
yield x
Not
Fibonacci dizisi, 0 ve 1 ile başlayan ve her sayının kendisinden önce gelen iki sayının toplanması ile elde edildiği bir sayı dizisidir. İtalyan matematikçi Leonardo Fibonacci’den adını alır. 0, 1, 1 (0+1), 2 (1+1), 3 (1+2), 5 (2+3), 8 (3+5), 13 (5+8), 21 (8+13), 34 (13+21) şeklinde devam eder.
Şimdi bu kodu çalıştıralım:
>>> f = fibonacci()
>>> next(f)
1
>>> next(f)
2
>>> next(f)
3
>>> next(f)
5
>>> next(f)
8
>>> next(f)
13
Gördüğünüz gibi üretecimiz bize (ilk 0 ve 1 sonrasındaki) fibonacci sayılarını vermektedir. Kodumuzu anlamaya çalışırsak:
İlk yinelemede, yani
next
fonksiyonunu ilk kullanışımızda,x
,y
vez
değişkenleri tanımlanıyor. Daha sonrawhile
döngüsüne giriliyor. Değişkenlerin değerleri değiştirildikten sonrayield x
deyimine geldiğimiz içinnext
fonksiyonux
değerini döndürürerek üretecemizin çalışmasını durduruyor.İkinci yinelememizde normal bir kodda olacağı gibi
while
döngümüzün başına gidiliyor. Aynı işlemler tekrarlanıyor. Tekraryield
deyimine geliniyor.x
değeri döndürürülüyor. Üretecimizin çalışması durduruluyor ve aynı şeyler tekrar etmeye devam ediyor.
Üreteçlerin çok güzel özelliklerinden biri de for
döngüsü ile kullanılabilmeleridir.
Örneğin fibonacci
üretecimiz için bunu uygulayalım:
>>> for i in fibonacci():
print(i)
1
2
3
5
8
13
21
34
55
89
144
...
Not
for i in fibonacci()
ifadesinde fibonacci
fonksiyonunu çağırdığımıza dikkat
edin. Sonuçta üretecimizin kendisi fibonacci
fonksiyonu değil, onun döndüreceği değer.
Ancak bu örnekte üretecimiz hiç durmuyor. Bazen üreteçlerimizin durmasını isteyebiliriz.
Bunu yapmamız için tek gereken şey üretecimizin durmasını istediğimiz yerde üretecimizi
return
etmemizdir. Sonuçta üreteçler de bir tür fonksiyondur ve return
deyimi
fonksiyonları sonlandırır (bu return
deyiminden dönen değer üreteçlerde bize ulaşmaz).
Bu durum next
fonksiyonunun StopIteration
yükseltmesine neden olur.
for
döngüsü bu hatayı yakalar ve üretecimizin bittiğini anlar:
def fibonacci():
x = 1
y = 0
z = 0
while True:
z = y
y = x
x = y + z
yield x
if x > 100:
return
>>> for i in fibonacci():
print(i)
1
2
3
5
8
13
21
34
55
89
144
>>>
Gördüğünüz gibi üretecimiz 100
’den büyük bir tane daha değer yazıp durdu. Tabii burada
fazladan bir if
kullanmak yerine bu şartı while
’dan sonra da yazabilirdik:
def fibonacci():
x = 1
y = 0
z = 0
while not x > 100:
z = y
y = x
x = y + z
yield x
Burada da x
değişkeni 100
’den büyük olduğunda döngümüz bitiyor ve başka kodumuz kalmadığımız için
fonksiyon sonlanıyor. Zaten bir fonksiyonun sonuna ulaşıldığında da biz bir değer döndürmediysek de
None
değeri döndürülecektir.
Son olarak parametre alan basit bir üreteç örneği yaparak bir sonraki konuya geçelim. Unutmayalım ki üreteçler de bir çeşit fonksiyon olduğu için fonksiyon tanımlarken yapabildiğimiz her şeyi üreteç tanımlarken de kullanabiliriz. Buna parametre vermek ve iç içe fonksiyonlar oluşturmak da dahildir.
Üretecimiz bir sayı
parametresi alacak ve o sayı
defa ekrana yazı yazdıracak:
def yaz(sayı):
for i in range(sayı):
print("Merhaba Dünya!")
yield
y = yaz(4)
for i in y:
print(i)
Kodun çıktısı:
Merhaba Dünya!
Merhaba Dünya!
Merhaba Dünya!
Merhaba Dünya!
‘yield from’ Deyimi¶
yield from
deyimi bir üretecin içinde, başka bir üretecin yield
ile
döndüreceği değerleri tekrar yield
etmek istediğimizde kullanılabilir.
Şöyle bir örnek verelim:
def üreteç1():
yield "üreteç1 başladı"
yield "üreteç1 bitti"
def üreteç2():
yield "üreteç2 başladı"
yield from üreteç1()
yield "üreteç2 bitti"
>>> for i in üreteç2():
print(i)
üreteç2 başladı
üreteç1 başladı
üreteç1 bitti
üreteç2 bitti
>>>
Aslında yield from
ile yazdığımız bu örnek şu kod ile eşdeğerdir:
def üreteç1():
yield "üreteç1 başladı"
yield "üreteç1 bitti"
def üreteç2():
yield "üreteç2 başladı"
for i in üreteç1():
yield i
yield "üreteç2 bitti"
>>> for i in üreteç2():
print(i)
üreteç2 başladı
üreteç1 başladı
üreteç1 bitti
üreteç2 bitti
>>>
Yani:
yield from bir_üreteç
ifadesi bu ifade eş değerdir:
for i in bir_üreteç:
yield i
Liste ve Sözlük Üreteçleri Hakkında¶
Üreteçler konusunun başında söylediğimiz şu bilgiyi tekrarlayarak konumuza başlayalım:
Biz üreteçlerle az çok tanışıyoruz. Liste üreteçleri olsun, sözlük üreteçleri olsun bu konu hakkında bir şeyler öğrenmiştik. Ancak biz üreteçlerimizi hep şunun gibi tanımlamıştık:
>>> listem = [i for i in range(10)] >>> listem [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]Dikkat ederseniz burada
i for i in range(10)
kısmı (nasıllambda
fonksiyonlar normal yolla tanımlanan fonksiyonlardan farklı bir söz dizimi kullanıyorsa) normal kodlardan biraz farklı bir söz dizimi kullanıyor. Bu söz dizimi ile karmaşık algoritmalar oluşturmak zordur, çoğunlukla da mümkün değildir. Zaten bunun bulunma sebebi karmaşık algoritmalarda kullanılması değil, kısa işlerde yazım kolaylığı sağlamasıdır. Yani bu yazım şekli, bazı fonksiyonlarınlambda
olarak tanımlanması gibi, üreteç tanımlamanın sadece kısa bir yoludur. Peki aslında üreteçler nasıl tanımlanır? Şimdi gelin bu konuyu inceleyelim.
Biz önceden üreteçleri şu şekilde kullanmayı biliyorduk:
>>> listem = [i for i in range(10)]
>>> listem
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Peki üreteç bu kodun neresinde? Aslında bu yazım oldukça kısaltılmış, yani kolaylaştırılmış bir yazım şeklidir. Biraz açacak olursak şunu elde ederiz:
>>> üreteç = (i for i in range(10))
>>> type(üreteç)
<class 'generator'>
>>> listem = list(üreteç)
>>> listem
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Şimdilik liste kısmını bir kenara koyarak üreteç kısmı ile ilgilenelim.
Gördüğünüz gibi aslında şu yazım şekli:
>>> üreteç = (i for i in range(10))
Bunun için bir kısaltmadır:
def üreteç_fonksiyonu():
for i in range(10):
yield i
üreteç = üreteç_fonksiyonu()
Aynı lambda
fonksiyonların normal fonksiyonlar için bir kısaltma olması gibi.
Ancak şuraya dikkat etmek lazım ki:
üreteç = (i for i in range(10))
Yazdığımızda, elimizde çağırıldığında bize üreteç döndürecek bir fonksiyonumuz yok.
Yani üreteç
değişkenimiz generator
türünde bir nesne ve tek kullanımlık.
Sonuçta üreteçlerin yinelenmesi bir defa bittikten sonra bir daha kullanamayız, çünkü bir defa bittikten sonra hep StopIteration
hatası yükseltirler. Eğer istersek yenisini oluşturabiliriz. üreteç
değişkenimizin yinelenmesi bir defa tamamlandıktan sonra daha fazla onu kullanamayacağımızı şu şekilde görebiliriz:
>>> üreteç = (i for i in range(5))
>>> for i in üreteç:
print(i)
0
1
2
3
4
>>> for i in üreteç:
print(i)
>>>
Gördüğünüz gibi üreteç
değişkenimizi bir defa for
döngüsü ile kullandığımızda
ikinci defa kullanamamaktayız. Çünkü ilk döngüde üretecimiz bitene kadar çalıştı
ve en sonunda StopIteration
yükseltti. Artık istediğimiz kadar üretecimizi
kullanmayı deneyelim, StopIteration
yükseltmeye devam edecektir (unutmayalım ki for
döngüsü
StopIteration
hatalarını yakalar ve yakaladığında da çalışmayı bırakır)
>>> üreteç = (i for i in range(3))
>>> next(üreteç)
0
>>> next(üreteç)
1
>>> next(üreteç)
2
>>> next(üreteç)
StopIteration
>>> next(üreteç)
StopIteration
Aynı şey normal yoldan tanımlanan üreteçler için de geçerlidir:
def üreteç_fonksiyonu():
for i in range(3):
yield i
>>> üreteç = üreteç_fonksiyonu()
>>> next(üreteç)
0
>>> next(üreteç)
1
>>> next(üreteç)
2
>>> next(üreteç)
StopIteration
>>> next(üreteç)
StopIteration
Buradaki fark üretecimizi bize veren fonksiyonumuz durduğu için yeni bir üreteç oluşturabiliyor olmamızdır:
>>> üreteç2 = üreteç_fonksiyonu()
>>> next(üreteç2)
0
Ancak şu şekilde bir tanımlama yaptığımızda:
>>> üreteç = (i for i in range(3))
>>> type(üreteç)
<class 'generator'>
Burada elde ettiğimiz üretecin kendisi oluyor, ve bu üreteç de tek kullanımlık. Şimdi bunların liste üreteçleri ile alakasına geri dönecek olursak:
>>> üreteç = üreteç_fonksiyonu()
>>> listem = list(üreteç)
>>> listem
[0, 1, 2]
Gördüğünüz gibi aslında normal yoldan tanımlanmış üreteçler, yani yield
ifadesi kullanılarak fonksiyon gibi tanımlanmış üreteçler, de list
fonksiyonuna argüman olarak verilebilir. Aynı
for
döngüsünde kullanılabilmesi gibi. Çünkü -kendi geliştirme arayüzünüzü kullanarak
görebilirsiniz- dikkat edersiniz list
fonksiyonunun ilk parametresinin adı iterable
’dır.
Türkçe’ye çevirirsek yinelenebilir. Biz zaten üreteçlerin yinelenebilir nesnelere örnek olduğunu
söylemiştik. Bu yüzden bütün üreteçleri list
fonksiyonunu kullanarak bir listeye çevirebiliriz.
Buna şu şekilde tanımlanan üreteçler de dahildir:
>>> üreteç = (i for i in range(3))
Bu yüzden şu kod güzel bir şekilde çalışmaktadır:
>>> üreteç = (i for i in range(3))
>>> list(üreteç)
[0, 1, 2]
Ve şu yazım da yukarıda yazdığımızın daha da kısaltılmış halinden başka bir şey değildir:
>>> listem = [i for i in range(3)]
Anlattıklarımız sözlük üreteçleri için de geçerlidir. Dikkat edersiniz kısa yoldan üreteç
tanımlamaları (i for i in range(3))
şeklinde, liste tanımlamaları [i for i in range(3)]
şeklinde
ve sözlük tanımlamaları da {str(i):i for i in range(3)}
şeklinde yapılmaktadır. Bu liste tanımlamasını:
>>> üreteç = (i for i in range(3))
>>> listem = list(üreteç)
Şu şekilde yazabileceğimiz gibi:
>>> üreteç = (i for i in range(3))
>>> listem = []
>>> for i in üreteç:
listem.append(i)
Bu sözlük tanımlamasını da:
>>> üreteç = ((str(i),i) for i in range(3))
>>> sözlük = dict(üreteç)
Şu şekilde yazabilirdik:
>>> üreteç = ((str(i),i) for i in range(3))
>>> sözlük = {}
>>> for key,value in üreteç:
sözlük[key] = value
Son örneğimizde üretecimiz her yinelenişinde iki elemanlı bir tuple
döndürüyor ve
bu demetin ilk elemanı for
döngüsü içinde key
değişkenine, ikinci elemanı ise
value
değişkenine atanıyor. Şunun gibi de düşünebilirsiniz:
>>> for key,value in (('0',0), ('1',1), ('2',2)):
sözlük[key] = value
Evet, artık üreteçler konusunda da kayda değer bilgiler öğrendiğimize göre bir sonraki konumuza geçelim.
Önemli Not
Sorularınızı yorumlarda dile getirmek yerine Yazbel Forumunda sorarsanız çok daha hızlı cevap alabilirsiniz.Belgelerdeki bir hata veya eksiği dile getirecekseniz lütfen yorumları kullanmak yerine Github'da bir konu (issue) açın.
Eğer yazdığınız yorum içinde kod kullanacaksanız kodlarınızı <pre><code> etiketleri içine alın. Örneğin:
<pre><code class="python"> print("Merhaba Dünya!") </code></pre>