Kurs
Python fonksiyonlarının her zaman bir return ifadesi olmak zorunda değildir. Jeneratör fonksiyonları, return yerine yield anahtar sözcüğünü içeren fonksiyonlardır.
Bu fonksiyonlar bir veri akışını temsil eden jeneratör yineleyiciler üretir. Bir yineleyici tarafından temsil edilen öğeler yalnızca gerektiğinde oluşturulur ve verilir. Bu tür değerlendirme genellikle tembel değerlendirme (lazy evaluation) olarak anılır.
Büyük veri kümeleriyle çalışırken, jeneratörler, öğelerinin her biri için bellekte yer gerektiren listeler, demetler ve diğer veri yapılarında veriyi saklamaya kıyasla bellek açısından verimli bir alternatif sunar. Jeneratör fonksiyonları ayrıca, listeler ve demetler gibi hevesli (eager) değerlendirilen yapılarla mümkün olmayan sonsuz yineleyiciler de oluşturabilir.
Başlamadan önce, fonksiyonlar ile jeneratörler arasındaki farkları kısaca hatırlayalım:
|
Özellik |
Fonksiyon |
Jeneratör |
|
Değer Üretimi |
Tüm değerleri bir kerede döndürür |
Değerleri talep üzerine, teker teker verir |
|
Yürütme |
Döndürmeden önce tamamen çalışır |
Değer verdikten sonra duraklar, bir sonraki değer istendiğinde devam eder |
|
Anahtar Sözcük |
return |
yield |
|
Bellek Kullanımı |
Potansiyel olarak yüksek; tüm diziyi bellekte tutar |
Düşük; yalnızca geçerli değeri ve bir sonraki için durumu tutar |
|
Yineleme |
Birden çok yineleme mümkün, ancak tüm dizinin saklanmasını gerektirir |
Tek geçişli yineleme için tasarlanmıştır; büyük veya sonsuz dizilerde daha verimlidir |
Python'un yield Anahtar Sözcüğünü Kullanarak Jeneratör Fonksiyonları Oluşturma
Python’da jeneratör terimi, bir jeneratör yineleyiciyi veya bir jeneratör fonksiyonunu ifade edebilir. Bunlar Python’da farklı fakat ilişkili nesnelerdir. Bu eğitimde karışıklığı önlemek için tam terimler sıkça kullanılmaktadır.
Önce jeneratör fonksiyonlarını inceleyelim. Bir jeneratör fonksiyonu, normal bir fonksiyona benzer görünür ancak return yerine yield anahtar sözcüğünü içerir.
Bir Python programı bir jeneratör fonksiyonunu çağırdığında, bir jeneratör yineleyici oluşturur. Yineleyiciler talep üzerine bir değer verir ve başka bir değer gerektiğinde yürütmeyi duraklatır. Bu kavramı açıklamak ve normal fonksiyonlarla jeneratör fonksiyonları arasındaki farkı göstermek için bir örneğe bakalım.
Normal bir fonksiyon kullanma
Önce, bir return ifadesi içeren normal bir fonksiyon tanımlayalım. Bu fonksiyon bir kelime dizisi ve bir harf alır ve her kelimedeki söz konusu harfin kaç kez geçtiğini içeren bir liste döndürür:
def find_letter_occurrences(words, letter):
output = []
for word in words:
output.append(word.count(letter))
return output
print(
find_letter_occurrences(["apple", "banana", "cherry"], "a")
)
[1, 3, 0]
Fonksiyon, listedeki 1, 3 ve 0 değerlerini çıktılar çünkü apple’da bir, banana’da üç ve cherry’de hiç a yok. Aynı fonksiyon, boş bir liste başlatıp .append() kullanmak yerine liste üreteçleriyle (list comprehension) yeniden yazılabilir:
def find_letter_occurrences(words, letter):
return [word.count(letter) for word in words]
Bu normal fonksiyon her çağrıldığında tüm sonuçları içeren bir liste döndürür. Ancak kelime listesi büyükse, bu normal fonksiyonu çağırmak bellek üzerinde baskı oluşturur; çünkü program, orijinaliyle aynı boyutta yeni bir liste oluşturur ve saklar. Bu fonksiyon birden çok giriş üzerinde tekrar tekrar kullanılıyorsa ya da benzer fonksiyonlar orijinal veriler üzerinde başka işlemler yapıyorsa bellek baskısı hızla artabilir.
Bir jeneratör fonksiyonu kullanma
Bunun yerine bir jeneratör fonksiyonu kullanılabilir:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
print(output)
<generator object find_letter_occurrences at 0x102935e00>
Fonksiyon return yerine yield anahtar sözcüğünü içerir. Bu jeneratör fonksiyonu çağrıldığında bir jeneratör nesnesi döndürür ve bu nesne output değişkenine atanır. Bu nesne bir yineleyicidir. Her kelimedeki harfin kaç kez geçtiğine dair veriyi içermez. Bunun yerine, jeneratör değerlere ihtiyaç duyulduğunda oluşturur ve verir. Şimdi bu jeneratör yineleyiciden ilk değeri alalım:
print(next(output))
1
next() yerleşik fonksiyonu, bir yineleyiciden bir sonraki değeri almanın yollarından biridir. Bu eğitimin ilerleyen kısımlarında diğer yöntemlere de bakacağız.
Jeneratör fonksiyonundaki kod, program yield anahtar sözcüğünün bulunduğu satıra ulaşana kadar yürütülür. Bu örnekte, for döngüsü ilk yinelemesine başlar ve words listesindeki ilk öğeyi alır. .count() string yöntemi bir tamsayı döndürür; bu durumda apple’da bir adet a olduğundan 1’dir. Jeneratör bu değeri verir ve next(output) tarafından döndürülür.
Jeneratör output bu noktada yürütmeyi duraklatır. Dolayısıyla jeneratör for döngüsünün ilk yinelemesini tamamlamış ve kelime listesindeki ilk kelimedeki a harfinin sayısını bulmuştur. Şimdi yeniden ihtiyaç duyulana kadar beklemektedir.
Yerleşik next() fonksiyonu output argümanıyla tekrar çağrılırsa, jeneratör duraklatıldığı yerden yürütmeye devam eder:
print(next(output))
3
Jeneratör, for döngüsünün ilk yinelemesindeki yield satırından devam eder. for döngüsünde başka satır olmadığından döngünün başına döner ve words listesinden ikinci öğeyi alır. Bu sefer .count() tarafından döndürülen değer 3’tür ve bu değer verilir. Jeneratör yürütmeyi bu noktada yeniden duraklatır.
Üçüncü next() çağrısı bu yürütmeyi sürdürür:
print(next(output))
0
İkinci yineleme for döngüsünün sonuna ulaşır ve üçüncü yinelemeye geçilir. Kod tekrar yield satırına ilerler ve bu kez cherry’de a geçmediği için 0 tamsayısını verir.
Jeneratör yine duraklar. Program bu jeneratörün akıbetini, next() fonksiyonunu dördüncü kez çağırdığımızda belirler:
print(next(output))
Traceback (most recent call last):
...
StopIteration
Yürütme, for döngüsünün üçüncü yinelemesinin sonundan devam eder. Ancak listede (words) artık öğe kalmadığı için döngü tamamlanmıştır. Jeneratör bir StopIteration hatası yükseltir.
Çoğu kullanımda jeneratör öğelerine doğrudan next() ile değil, başka bir yineleme süreciyle erişilir. StopIteration istisnası, yineleme sürecinin sona erdiğini bildirir. Bunu bu eğitimin bir sonraki bölümünde daha ayrıntılı inceleyeceğiz.
Önceki örnekte olduğu gibi, işlemi tek bir ifadeyle temsil edilebildiğinde Python’un jeneratör yineleyicileri oluşturmak için başka bir yolu daha vardır. output jeneratör yineleyicisi bir jeneratör ifadesi kullanılarak da oluşturulabilir:
words = ["apple", "banana", "cherry"]
letter = "a"
output = (word.count(letter) for word in words)
print(next(output))
print(next(output))
print(next(output))
print(next(output))
1
3
0
Traceback (most recent call last):
...
StopIteration
output değişkenine atanan parantez içindeki ifade bir jeneratör ifadesidir ve find_letter_occurrences() jeneratör fonksiyonunun ürettiğine benzer bir jeneratör yineleyici oluşturur.
Bu bölümü, her bir öğeye ihtiyaç duyulduğunda yürütmenin nasıl duraklayıp devam ettiğini vurgulamak için bir başka jeneratör fonksiyonu örneğiyle bitirelim:
def show_status():
print("Start")
yield
print("Middle")
yield
print("End")
yield
status = show_status()
next(status)
Start
Bu jeneratör fonksiyonunda döngü yoktur. Bunun yerine yield anahtar sözcüğünü içeren üç satır vardır. Kod, show_status() jeneratör fonksiyonu çağrıldığında bir jeneratör yineleyici status oluşturur. Program next(status)i ilk kez çağırdığında jeneratör çalışmaya başlar. "Start" dizesini yazdırır ve ilk yield ifadesinden sonra duraklar. yield anahtar sözcüğünü bir nesne izlemediği için jeneratör None verir.
Program "Middle" dizesini yalnızca next() ikinci kez çağrıldığında yazdırır:
next(status)
Middle
Jeneratör ikinci yield ifadesinden sonra duraklar. next() üçüncü kez çağrıldığında son dize olan "End" yazdırılır:
next(status)
End
Jeneratör son yield ifadesinde duraklar. Program bu jeneratör yineleyiciden bir değer istediğinde bir StopIteration hatası yükseltir:
next(status)
Traceback (most recent call last):
...
StopIteration
Jeneratörleri kullanmanın daha fazla yolunu bir sonraki bölümde keşfedeceğiz.
Jeneratör Yineleyicilerle Çalışma
Jeneratör fonksiyonları jeneratör yineleyiciler oluşturur ve yineleyiciler yinelenebilirdir (iterable). Bu ifadeyi açalım. Program her jeneratör fonksiyonu çağırdığında bir yineleyici oluşturur. Yineleyiciler yinelenebilir olduğundan, for döngülerinde ve diğer yinelemeli süreçlerde kullanılabilirler.
Dolayısıyla next() yerleşik fonksiyonu bir yineleyicideki öğelere erişmenin tek yolu değildir. Bu bölüm jeneratörlerle çalışmanın diğer yollarını inceler.
Jeneratör yineleyicilerle Python’un yineleme protokolünü kullanma
Bu eğitimdeki önceki bir bölümde yer alan bir jeneratör fonksiyonuna geri dönelim:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
for value in output:
print(value)
1
3
0
next()i birkaç kez kullanmak yerine, bu sürümde output jeneratör yineleyicisi bir for döngüsünde kullanılıyor. Yineleyiciler yinelenebilir olduğundan for döngülerinde kullanılabilirler. Döngü, jeneratör yineleyiciden değer kalmayana kadar öğeleri çeker.
Listeler ve demetler gibi veri yapılarından farklı olarak bir yineleyici yalnızca bir kez kullanılabilir. Aynı for döngüsünü ikinci kez çalıştırmaya çalışırsak kod değerleri yeniden yazdırmaz:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
output = find_letter_occurrences(words, letter)
print("First attempt:")
for value in output:
print(value)
print("Second attempt:")
for value in output:
print(value)
First attempt:
1
3
0
Second attempt:
Yineleyici ilk for döngüsü tarafından tüketildiği için artık değer veremez. Jeneratöre tükendikten sonra yeniden ihtiyaç duyulursa, jeneratör fonksiyonundan yeni bir jeneratör yineleyici oluşturmamız gerekir.
Bir programda aynı anda birden fazla jeneratör yineleyicinin bulunması da mümkündür:
def find_letter_occurrences(words, letter):
for word in words:
yield word.count(letter)
words = ["apple", "banana", "cherry"]
letter = "a"
first_output = find_letter_occurrences(words, letter)
second_output = find_letter_occurrences(words, letter)
print("First value of first_output:")
print(next(first_output))
print("Values of second_output:")
for value in second_output:
print(value)
print("Remaining values of first_output:")
for value in first_output:
print(value)
First value of first_output:
1
Values of second_output:
1
3
0
Remaining values of first_output:
3
0
find_letter_occurrences() jeneratör fonksiyonu iki jeneratör yineleyici oluşturur: first_output ve second_output. Her iki yineleyici de words listesindeki aynı verilere atıf yapsa da birbirlerinden bağımsız ilerlerler.
Bu örnek, first_outputtan ilk değeri next() ile alır. Jeneratör yineleyici 1’i verir ve bu noktada duraklar. Program daha sonra second_output üzerinde döngüye girer. Bu jeneratör henüz hiç değer vermediği için, döngü ikinci yineleyici tarafından verilen tüm değerleri dolaşır. Son olarak, first_output üzerinde yineleme yapan bir başka for döngüsü vardır. Ancak bu yineleyici programın başlarında ilk değerini zaten vermiştir. Döngü bu yineleyicideki kalan değerleri dolaşır.
for döngüsü, jeneratör yineleyiciler üzerinde yineleme yapmak için kullanılabilecek tek süreç değildir:
print(*find_letter_occurrences(words, letter))
print(sorted(find_letter_occurrences(words, letter)))
1 3 0
[0, 1, 3]
Bu örneklerde, jeneratör yineleyiciyi bir değişkene atamak yerine, program jeneratör fonksiyonunu doğrudan çağırarak oluşturur ve kullanır. İlk örnekte yineleyici yıldız gösterimiyle açılır (unpack edilir). Bu süreç, for döngüsüyle aynı yineleme protokolüne dayanır.
İkinci örnekte jeneratör yineleyici, yinelenebilir bir argüman gerektiren yerleşik sorted()a geçirilir. Jeneratörler yinelenebilirdir; bu nedenle Python’da yineleme gerçekleştiği her yerde kullanılabilirler.
Sonsuz yineleyiciler oluşturma
Bir jeneratör bir değer verir ve bir sonraki değere ihtiyaç duyulana kadar duraklar. Kod bir yineleyiciden her değer istediğinde, jeneratör fonksiyonu içindeki kod bir sonraki yield ifadesi değerlendirilene kadar çalışır. Bu eğitimdeki tüm örneklerde, jeneratör fonksiyonunda sonlu sayıda yield ifadesi vardı. Ancak jeneratör fonksiyonu içinde bir while döngüsü kullanarak sonsuz sayıda değer veren bir jeneratör oluşturmak mümkündür. Aşağıdaki örnekte jeneratör, kendisine geçirilen renkler listesinden rastgele bir renk verir:
import random
def get_color(colors):
while True:
yield random.choice(colors)
output_colors = get_color(["red", "green", "blue"])
print("First two colors:")
print(next(output_colors))
print(next(output_colors))
print("Next 10 colors using a 'for' loop:")
for _ in range(10):
print(next(output_colors))
First two colors:
green
red
Next 10 colors using a 'for' loop:
blue
green
green
green
red
red
red
blue
green
red
get_color() jeneratör fonksiyonu bir while döngüsü içinde bir yield ifadesine sahiptir. Dolayısıyla kod, bir sonraki değeri ararken her zaman yeni bir yield ifadesiyle karşılaşacaktır. output_colors jeneratör yineleyicisi, giriş listesinden rastgele seçilen sonsuz sayıda renk verir. Bu jeneratör asla tükenmez.
Listeler ve demetler gibi sonsuz veri yapıları oluşturmak mümkün değildir. Jeneratörler, bir programın sonsuz yinelenebilirler oluşturmasını sağlar. Jeneratör yineleyici doğrudan bir for döngüsünde kullanılırsa, döngünün sonsuza kadar çalışacağını unutmayın.
İleri Jeneratör Kavramları
Jeneratörlerin Python’da daha gelişmiş kullanım alanları vardır. Bu bölüm bunlardan bazılarını inceleyecek.
Jeneratöre bir nesne gönderme
Jeneratörler ayrıca kod değerlendirilirken kullanılabilecek ek verileri de kabul edebilir. yield anahtar sözcüğünü içeren ifade, bir değere değerlendiren bir ifadedir. Bu değer jeneratör fonksiyonu içinde bir değişkene atanabilir. Bu kavramı göstermek için basit bir örnekle başlayalım:
def generator_function():
value = yield 1
print(f"The yield expression evaluates to: {value}")
value = yield 2
print(f"The yield expression evaluates to: {value}")
output = generator_function()
print(next(output))
print(next(output))
print(next(output))
1
The yield expression evaluates to: None
2
The yield expression evaluates to: None
Traceback (most recent call last):
...
StopIteration
Python’daki yield anahtar sözcüğü, bir değere değerlendiren bir ifade oluşturur. Ancak jeneratör fonksiyonu içindeki bu ifadenin değeri, jeneratör tarafından verilen nesneyle aynı değildir. İlk yield ifadesini ele alalım. Jeneratör 1 tamsayısını verir. Dolayısıyla print(next(output)) ilk çağrıldığında 1’i gösterir ve jeneratörün yürütmesi duraklar.
Bununla birlikte, jeneratör içindeki yield ifadesi bir nesneye değerlendirilir ve bu nesne value adlı değişken adına atanır. Bu örnekte yield, value değişkenine None atar. Bu süreç jeneratör fonksiyonundaki yieldün ikinci geçişi için de tekrarlanır. Üçüncü next() çağrısının amacı, jeneratör fonksiyonundaki tüm kodun yürütüldüğünden emin olmaktır.
İkinci ve üçüncü next() çağrılarını, jeneratör sınıfındaki bir yöntem olan .send() ile değiştirelim:
def generator_function():
value = yield 1
print(f"The yield expression evaluates to: {value}")
value = yield 2
print(f"The yield expression evaluates to: {value}")
output = generator_function()
print(next(output))
print(output.send("Here's a value"))
print(output.send("Here's another value"))
1
The yield expression evaluates to: Here's a value
2
The yield expression evaluates to: Here's another value
Traceback (most recent call last):
...
StopIteration
Jeneratör fonksiyonu değişmedi. Jeneratör next() çağrılarak başlatılır ve kod ilk olarak 1 tamsayısını verene kadar çalışır. İkinci kez next() kullanmak yerine program output.send() çağrısını yapar. Bu yöntem, jeneratöre bir nesne gönderir. Bu örnekte nesne bir stringdir. Jeneratör fonksiyonundaki yield ifadesi bu stringe değerlendirilir ve value değişkenine atanır. Böylece jeneratör bu stringi kendi kodunda kullanabilir.
İkinci .send() çağrısı jeneratöre yeni bir nesne gönderir ve bu nesne aynı value değişkenine atanır. Son print() çağrısından sonra başka yield ifadesi kalmadığı için jeneratör bir StopIteration yükseltir.
.send() kullanımına başka bir örneğe bakalım. Aşağıdaki jeneratör bir hesaptaki bakiyeyi gösterir, ancak bakiye güncellenebilir:
def get_balance(start_balance):
balance = start_balance
while True:
amount = yield balance
if amount is not None:
balance += amount
current_balance = get_balance(100)
print(next(current_balance))
print(current_balance.send(10))
print(current_balance.send(-20))
print(next(current_balance))
100
110
90
90
Jeneratör fonksiyonu çağrıldığında bir başlangıç bakiyesi gerektirir. balanceın değeri, jeneratör kodu yürüttükçe değişebilir. .send() kullanılarak jeneratöre gönderilen her nesne amount değişkenine atanır. Bu değişken, eğer jeneratör bir değer verirken kendisine bir nesne gönderilmemişse None olur; gönderilmişse .send() ile yollanan nesneyi içerir.
current_balance jeneratör yineleyicisi 100 $ bakiyeyle başlar. Jeneratör, ilk değeri verene kadar kodu başlatan next() çağrısıyla başlatılır.
Jeneratör bir kez başlatıldıktan sonra, yürütmeyi next() yerine .send() ile de sürdürebilirsiniz. Jeneratör, gönderilen değeri bakiyeye ekler. Herhangi bir değer gönderilmezse, örneğin tekrar next() çağrılırsa, jeneratör değişmemiş bakiyeyi verir.
Başka bir yinelenebilirden doğrudan değer vermek
Python jeneratörleri, yield from sözdizimini kullanarak başka bir jeneratörden veya yinelenebilirden doğrudan değerler de verebilir. İç içe geçmiş bir listeden değerler veren bir jeneratör fonksiyonu örneğine bakalım:
def flatten(nested_list):
for item in nested_list:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
nested_list = [1, [2, 3], [4, [5, 6]], 7]
print(list(flatten(nested_list)))
[1, 2, 3, 4, 5, 6, 7]
Jeneratör fonksiyonu, içinde iç içe listeler barındırabilen bir liste alır. for döngüsü listedeki öğeleri yineler. Dış listedeki her öğe ya bir değer (bu örnekte tamsayı) ya da başka bir listedir. Öğenin liste olmadığı durumda jeneratör öğeyi verir.
Ancak öğe bir listese, jeneratör, iç listeyi argüman olarak vererek flatten() jeneratör fonksiyonunu özyineli (recursive) olarak yeniden çağırır. Bu, veri kaynağı olarak iç listeyi kullanan başka bir jeneratör yineleyici oluşturur. Bu satır bir yield ifadesi kullansaydı, ilk jeneratör ikinci jeneratörü verirdi. Bunun yerine yield from kullanarak, ilk jeneratör ikinci jeneratörden gelen değerleri verir.
Özet: yield ve return
return ve yield ile tanımlanan fonksiyonlar benzer görünse de davranışları farklıdır. Temel farkları özetleyelim:
|
Normal Fonksiyon |
Jeneratör Fonksiyonu |
|
|
Anahtar Sözcük |
return (açıkça kullanılmasa da örtük olarak) |
yield |
|
Çağrıldığında |
return’e ulaşana kadar kodu yürütür, ardından nihai değeri döndürür |
Bir jeneratör yineleyici oluşturur |
|
Sonlanma |
return ifadesiyle sonlanır |
yield ile duraklar, daha sonra devam edebilir |
|
Döndürülen Değer |
Tek bir nesne (bir veri yapısı olabilir) |
Jeneratör yineleyici |
|
Yield İfadesi |
Uygulanamaz (bir ifade oluşturur) |
None’a veya .send() ile gönderilen değere değerlendirir |
|
Kullanım Senaryoları |
Nihai bir sonuç döndürmek için idealdir |
Özellikle büyük veya sonsuz diziler için bir veri akışı oluşturmakta idealdir |
Sonuç
Python’un yield anahtar sözcüğü, bir jeneratör fonksiyonu tanımlamak için fonksiyonlarda kullanılır. Bu fonksiyonlar çağrıldığında jeneratör yineleyiciler oluşturur. Jeneratörler, ifadelerin, ifade yürütüldüğünde değil değere ihtiyaç duyulduğunda değerlendirildiği, Python’daki tembel değerlendirmenin bir örneğidir. Bu nedenle yield ifadesi, değerlerin bellekte saklanmasına gerek kalmadan talep üzerine üretildiği bir veri akışı oluşturmak için kullanışlıdır.
Büyük veri kümeleriyle çalışırken ve çok sayıda işlem gerektiğinde verimlilik önemli bir husustur. Python’un jeneratör yineleyicileri, büyük miktarda veriyi verimli bir şekilde işlemek için gereken temel araçlardan biridir.
Python hakkında daha fazla bilgi edinmek istiyorsanız, şu Python Developer kariyer yoluna göz atın.
Malta Üniversitesi’nde lisans düzeyinde Fizik ve Matematik okudum. Ardından Londra’ya taşındım ve Imperial College’da Fizik alanında doktoramı tamamladım. İnsan retinasını görüntülemek için yeni optik teknikler üzerine çalıştım. Şimdi Python hakkında yazmaya, Python’u anlatmaya ve Python öğretmeye odaklanıyorum.

