Python'da generator (üreteç), gövdesinde yield geçen bir fonksiyonun çağrılmasıyla elde edilen ve değerleri tek tek, ancak istendiğinde üreten bir iterator nesnesidir. Fonksiyonu çağırdığınızda gövde hemen çalışmaz; her next() çağrısında kod bir sonraki yield ifadesine kadar ilerler, değeri verir ve tüm yerel durumunu koruyarak bekler. Böylece milyonlarca öğeyi belleğe almadan işleyebilir, sonsuz diziler tanımlayabilir ve adım adım çalışan veri hatları kurabilirsiniz.

Bu yazıda önce generator'ların üzerine kurulduğu iterator protokolünü (iter(), next(), StopIteration) açıklıyor, ardından generator fonksiyonlarını, generator ifadelerini, yield from ile yetki devrini, send()/close() yöntemlerini ve sık yapılan hataları ele alıyoruz. Tüm örnekler Python 3.15.2 ile çalıştırıldı. Serinin genel haritası için Python öğrenme rehberi sayfasına bakabilirsiniz.

Iterable ve iterator: farkı nedir?

Python'da for döngüsünün üzerinde dolaşabildiği her nesneye iterable (yinelenebilir) denir: liste, tuple, str, dict, set, dosya nesnesi ve range bunlara örnektir. Iterator ise akışı temsil eden ayrı bir nesnedir: __next__() yöntemi her çağrıldığında bir sonraki öğeyi döndürür, öğe kalmadığında StopIteration istisnası yükseltir. Yerleşik iter() fonksiyonu bir iterable'dan iterator üretir; nesne yinelemeyi desteklemiyorsa TypeError verir.

Protokolün önemli bir kuralı, iterator'ın __iter__() yönteminin nesnenin kendisini döndürmesidir. Bu sayede bir iterator'ı doğrudan for döngüsüne verebilirsiniz. Listenin kendisi ise iterator değildir; her iter() çağrısında yeni ve bağımsız bir iterator üretir. Aşağıdaki örnekte bu farkı görebilirsiniz. next() fonksiyonuna ikinci argüman verirseniz, akış bittiğinde istisna yerine bu varsayılan değer döner.

python
sayilar = [10, 20, 30]
it = iter(sayilar)
print(next(it))
print(next(it))
print(next(it))
print(next(it, 'bitti'))
print(iter(it) is it)
print(iter(sayilar) is sayilar)

# çıktı: 10
# çıktı: 20
# çıktı: 30
# çıktı: bitti
# çıktı: True
# çıktı: False

Iterator'lar yalnızca ileri gider. Resmî Functional Programming HOWTO bunu açıkça vurgular: protokol yalnızca __next__() yöntemini şart koşar; önceki öğeye dönmenin, iterator'ı sıfırlamanın ya da kopyalamanın standart bir yolu yoktur. Aynı veriyi ikinci kez dolaşmanız gerekiyorsa yeni bir iterator oluşturmanız gerekir. for döngüsü arka planda tam olarak bunu yapar: önce iter() çağırır, sonra StopIteration gelene kadar next() ile ilerler. Döngü sözdiziminin ayrıntıları için Python for ve while döngüleri yazısına bakabilirsiniz.

Sınıfla iterator yazmak

Kendi iterator'ınızı yazmanın klasik yolu, __iter__() ve __next__() yöntemlerini tanımlayan bir sınıftır. Resmî Python Tutorial'daki örneğe benzer şekilde, aşağıdaki sınıf verilen sayıdan geriye doğru sayar. Durumu (self.n) nesnenin özniteliğinde tutmanız ve sonunda StopIteration yükseltmeyi unutmamanız gerekir.

python
class GeriSayim:
    def __init__(self, baslangic):
        self.n = baslangic

    def __iter__(self):
        return self

    def __next__(self):
        if self.n <= 0:
            raise StopIteration
        self.n -= 1
        return self.n + 1

for x in GeriSayim(3):
    print(x)

# çıktı: 3
# çıktı: 2
# çıktı: 1

Bu yaklaşım çalışır ama oldukça fazla tören içerir: durumu elle saklarsınız, bitiş koşulunu elle kontrol edersiniz, istisnayı elle yükseltirsiniz. Özel metotların genel mantığı için Python özel metotlar (dunder) yazısına bakabilirsiniz. Generator fonksiyonları ise aynı işi çok daha kısa yoldan yapar.

Generator fonksiyonu ve yield

Language Reference'a göre bir fonksiyonun gövdesinde yield ifadesi bulunması, o fonksiyonu generator fonksiyonuna dönüştürür. Böyle bir fonksiyonu çağırdığınızda gövde çalışmaz; bunun yerine generator adı verilen bir iterator nesnesi döner. Çalışma, generator'ın yöntemlerinden biri (genellikle next() üzerinden __next__()) çağrıldığında başlar ve ilk yield ifadesine kadar sürer. Orada fonksiyon askıya alınır: yerel değişkenler, kodun nerede kaldığı ve istisna işleme durumu dahil her şey korunur. Bir sonraki çağrıda kaldığı yerden devam eder.

python
def geri_sayim(n):
    print('başladı')
    while n > 0:
        yield n
        n -= 1
    print('bitti')

g = geri_sayim(2)
print(type(g).__name__)
print(next(g))
print(next(g))
try:
    next(g)
except StopIteration:
    print('StopIteration geldi')

# çıktı: generator
# çıktı: başladı
# çıktı: 2
# çıktı: 1
# çıktı: bitti
# çıktı: StopIteration geldi

Çıktıya dikkat edin: "başladı" yazısı generator oluşturulduğunda değil, ilk next() çağrısında görünüyor. "bitti" ise döngü sona erip fonksiyon gövdesinin sonuna ulaşıldığında, yani üçüncü next() çağrısında yazdırılıyor; hemen ardından StopIteration yükseliyor. Generator fonksiyonu return ile ya da gövdenin sonuna gelerek bittiğinde Python otomatik olarak StopIteration üretir; sizin yükseltmeniz gerekmez.

Generator'lar __iter__() ve __next__() yöntemlerini otomatik olarak edinir. Bu yüzden sınıflı geri sayım örneğini dört satırlık bir fonksiyona indirebilirsiniz. Tutorial da aynı noktayı vurgular: sınıf tabanlı iterator'larla yapılabilen her şey generator'larla da yapılabilir, ama generator'lar daha kompakt olur çünkü yerel değişkenler ve çalışma durumu çağrılar arasında kendiliğinden saklanır.

Generator ifadeleri

Generator ifadeleri: Sözdizimi, Ne zaman hesaplanır, Bellek, Tekrar dolaşma

Basit dönüşümler için ayrı bir fonksiyon yazmanız gerekmez. Generator ifadesi, list comprehension ile aynı sözdizimini kullanır ama köşeli parantez yerine normal parantez kullanır: (x * x for x in range(4)). Sonuç bir liste değil, değerleri talep edildikçe hesaplayan bir generator'dır. Fonksiyona tek argüman olarak verildiğinde dıştaki parantezleri yazmanıza gerek yoktur: sum(x * 2 for x in range(5)) geçerlidir. Ancak birden fazla argüman varsa generator ifadesini parantez içine almanız gerekir; aksi halde Python 3.15.2 "Generator expression must be parenthesized" iletisiyle SyntaxError verir.

python
import sys

liste = [x * 2 for x in range(1_000_000)]
uretec = (x * 2 for x in range(1_000_000))
print(sys.getsizeof(liste))
print(sys.getsizeof(uretec))
print(sum(x * 2 for x in range(5)))
try:
    compile('print(x for x in range(3), 1)', 'ornek.py', 'exec')
except SyntaxError as e:
    print(e.msg)

# çıktı: 8448728
# çıktı: 216
# çıktı: 20
# çıktı: Generator expression must be parenthesized

İlk iki satırın değerleri platforma göre değişebilir, ama aradaki fark kavramı iyi gösterir: bir milyon öğeli liste bütün öğeleri bellekte tutarken generator ifadesi yalnızca küçük bir nesneden ibarettir, çünkü değerleri sırası geldikçe üretir. Sonucu yalnızca bir kez dolaşacak ve bir toplama, en büyük değer arama ya da filtreleme gibi bir işlemden geçirecekseniz generator ifadesi doğal seçimdir. Comprehension sözdiziminin ayrıntıları Python list comprehension yazısında anlatılıyor.

ÖzellikList comprehensionGenerator ifadesi
Sözdizimi[x * 2 for x in veri](x * 2 for x in veri)
Ne zaman hesaplanırHemen, tamamıTalep edildikçe, tek tek
BellekTüm öğeler bellekteYalnızca o anki durum
Tekrar dolaşmaİstediğiniz kadarBir kez; sonra tükenir
len() ve indeksDesteklerDesteklemez (TypeError)
Sonsuz kaynakKullanılamazislice ile kullanılabilir

Python 3.15: generator ifadesinde açma (*)

Python 3.15 ile gelen PEP 798, comprehension'larda ve generator ifadelerinde * ile açmaya izin verir. What's New belgesine göre (*L for L in listeler) ifadesi, iç içe (x for L in listeler for x in L) yazımının kısa karşılığıdır. Aynı değişiklik asenkron generator ifadelerini de kapsar. Bu sürümün diğer yenilikleri için Python 3.15 yenilikleri yazısına bakın.

python
listeler = [[1, 2], [3], [4, 5]]
gen = (*L for L in listeler)
print(list(gen))

# çıktı: [1, 2, 3, 4, 5]

Tükenme: generator yalnızca bir kez dolaşılır

Tükenme: generator yalnızca bir kez dolaşılır

Generator'lar ile ilgili en sık karşılaşılan şaşkınlık, ikinci dolaşmada hiçbir şey dönmemesidir. Generator bir iterator olduğu için sonuna ulaştıktan sonra boş kalır; ikinci sum() çağrısı 0, list() çağrısı ise boş liste döndürür. Hata vermez, sessizce boş döner; bu yüzden fark edilmesi zordur.

python
kareler = (x * x for x in range(4))
print(sum(kareler))
print(sum(kareler))
print(list(kareler))

# çıktı: 14
# çıktı: 0
# çıktı: []

Çözüm basittir: değerleri birden fazla kez kullanacaksanız ya sonucu bir listeye çevirip saklayın ya da her kullanımda generator'ı yeniden oluşturun. Generator fonksiyonu kullanıyorsanız ikinci yol çok kolaydır; fonksiyonu yeniden çağırmanız yeterlidir. Aşağıdaki veri hattı örneğinde ayir(temiz(satirlar)) ifadesi iki kez yazılmıştır; bu kasıtlıdır, çünkü ilk zincir list() tarafından tüketilmiştir.

Veri hattı kurmak: generator'ları zincirlemek

Generator'ların en güçlü kullanımlarından biri, her biri tek bir iş yapan küçük adımları birbirine bağlamaktır. Her adım bir önceki adımın çıktısını tüketir ve kendi çıktısını üretir. Hiçbir aşama verinin tamamını belleğe almaz; bir satır, bütün hattan geçip işlendikten sonra sıradaki satır okunur. Dosya nesneleri de iterable olduğu için gerçek bir projede satirlar listesinin yerine açık bir dosyayı verebilirsiniz; dosya okuma ayrıntıları Python dosya okuma ve yazma yazısında.

python
satirlar = ['# stok listesi', 'elma,3', '', 'armut,5', 'kiraz,2']

def temiz(satirlar):
    for s in satirlar:
        s = s.strip()
        if s and not s.startswith('#'):
            yield s

def ayir(satirlar):
    for s in satirlar:
        ad, adet = s.split(',')
        yield ad, int(adet)

print(list(ayir(temiz(satirlar))))
print(sum(adet for _, adet in ayir(temiz(satirlar))))

# çıktı: [('elma', 3), ('armut', 5), ('kiraz', 2)]
# çıktı: 10

Bu yapının iki avantajı vardır. Birincisi okunabilirlik: temiz yalnızca boş ve yorum satırlarını atar, ayir yalnızca ayrıştırır. İkincisi tembellik: hattın sonundaki tüketici (burada sum() ya da list()) değer istemedikçe hiçbir adım çalışmaz. Gerçek CSV dosyaları için kendi ayrıştırıcınızı yazmak yerine Python csv modülü kullanmanız daha güvenlidir; buradaki örnek yalnızca deseni göstermek içindir.

Sonsuz generator ve itertools.islice

Generator değerleri talep edildikçe ürettiği için sonsuz bir dizi tanımlamak tamamen meşrudur. HOWTO belgesi de iterator'ların sonlu olmak zorunda olmadığını belirtir. Dikkat etmeniz gereken nokta tüketicidir: list(), max(), min() ya da in operatörünü sonsuz bir generator üzerinde kullanırsanız program asla dönmez. Sınırlı sayıda değer almak için itertools.islice() kullanın.

python
from itertools import islice

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

print(list(islice(fibonacci(), 10)))

# çıktı: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

islice, count, chain ve batched gibi iterator araçları Python itertools ve functools yazısında ayrıntılı anlatılıyor. Bu araçların hepsi tembel çalışır, yani generator'larla doğal biçimde birleşir.

yield from ile alt generator'a yetki devretmek

Python 3.3 ile gelen yield from ifade sözdizimi, verilen iterable'ın ürettiği bütün değerleri doğrudan dış generator'ın çağıranına aktarır. Language Reference'a göre send() ile gönderilen değerler ve throw() ile atılan istisnalar da alt iterator destekliyorsa ona iletilir. Alt generator bir değer return ettiğinde, bu değer StopIteration istisnasının value özniteliği üzerinden yield from ifadesinin sonucu olur.

python
def alt():
    yield 1
    yield 2
    return 'alt bitti'

def ust():
    sonuc = yield from alt()
    print('alt üreteç döndürdü:', sonuc)
    yield 3

print(list(ust()))

def duzlestir(veri):
    for oge in veri:
        if isinstance(oge, list):
            yield from duzlestir(oge)
        else:
            yield oge

print(list(duzlestir([1, [2, [3, 4]], 5])))

# çıktı: alt üreteç döndürdü: alt bitti
# çıktı: [1, 2, 3]
# çıktı: [1, 2, 3, 4, 5]

İkinci örnek, iç içe listeleri özyinelemeli olarak düzleştiren klasik bir kullanım. yield from olmadan aynı işi yapmak için iç generator'ı bir for döngüsüyle dolaşıp her öğeyi tek tek yield etmeniz gerekirdi. Basit değer aktarımında iki yazım aynı sonucu verir; fark, send(), throw() ve dönüş değeri gibi ileri özelliklerin de doğru biçimde iletilmesindedir. Not: yield from asenkron generator fonksiyonları içinde kullanılamaz; orada sözdizimi hatasıdır.

send(), throw() ve close(): generator ile iki yönlü iletişim

yield aslında bir ifadedir ve bir değer döndürür. Generator next() ile devam ettirilirse bu değer None olur; send(deger) ile devam ettirilirse gönderilen değer olur. Bu özellik generator'ı, dışarıdan veri alan küçük bir durum makinesine çevirir. Aşağıdaki örnek, gönderilen her sayıdan sonra güncel ortalamayı veren bir generator'dır.

python
def ortalama():
    toplam = 0
    adet = 0
    ort = None
    try:
        while True:
            deger = yield ort
            toplam += deger
            adet += 1
            ort = toplam / adet
    finally:
        print('temizlik yapıldı')

o = ortalama()
next(o)
print(o.send(10))
print(o.send(20))
print(o.send(60))
o.close()

y = ortalama()
try:
    y.send(5)
except TypeError as e:
    print(e)

# çıktı: 10.0
# çıktı: 15.0
# çıktı: 30.0
# çıktı: temizlik yapıldı
# çıktı: can't send non-None value to a just-started generator

Burada üç ayrıntıya dikkat edin. Birincisi, generator'ı ilk yield'e kadar ilerletmek için önce next(o) çağrılır. Yeni oluşturulmuş bir generator'a None dışında bir değer göndermeye çalışırsanız, son satırlarda görüldüğü gibi TypeError alırsınız. İkincisi, close() çağrısı generator'ın askıda olduğu noktada GeneratorExit yükseltir; böylece finally bloğu çalışır ve temizlik yapılır. Üçüncüsü, throw() yöntemi generator'ın içine, askıda olduğu noktadan bir istisna fırlatır; generator bunu yakalayıp devam edebilir ya da sonlanabilir.

Language Reference, yield ifadelerinin try bloğunun her yerinde kullanılabileceğini ve generator sonlandırılmadan önce devam ettirilmezse (referans sayısı sıfıra düştüğünde ya da çöp toplayıcı tarafından toplandığında) close() yönteminin otomatik çağrılacağını belirtir. Yani bekleyen finally blokları sonunda çalışır; yine de dosya ya da bağlantı gibi kaynakları ne zaman serbest bırakacağınızı kesin bilmek istiyorsanız close() çağrısını kendiniz yapın veya kaynağı with bloğu ve context manager ile yönetin. Kapatılmış bir generator'dan yeni değer gelmez:

python
def gen():
    try:
        yield 1
        yield 2
    finally:
        print('finally çalıştı')

g = gen()
print(next(g))
g.close()
print(list(g))

# çıktı: 1
# çıktı: finally çalıştı
# çıktı: []

Generator içinde StopIteration yükseltmeyin

Eski kodlarda generator'ı erken bitirmek için raise StopIteration yazıldığını görebilirsiniz. Python 3.7'den beri PEP 479 tüm kodlar için varsayılan olarak etkindir: generator kodu doğrudan ya da dolaylı olarak StopIteration yükseltirse, bu istisna RuntimeError'a dönüştürülür ve orijinal istisna yeni hatanın nedeni (__cause__) olarak saklanır. Dolaylı durum özellikle sinsidir: generator içinde boş bir iterator üzerinde next() çağırmak da aynı sonucu doğurur.

python
def hatali():
    it = iter([])
    yield next(it)

try:
    list(hatali())
except RuntimeError as e:
    print(type(e).__name__ + ':', e)
    print('nedeni:', type(e.__cause__).__name__)

# çıktı: RuntimeError: generator raised StopIteration
# çıktı: nedeni: StopIteration

Generator'ı bitirmek istediğinizde basitçe return yazın. İçeride next() çağırıyorsanız ya varsayılan değerli biçimi (next(it, None)) kullanın ya da StopIteration istisnasını yakalayıp return ile çıkın. İstisna hiyerarşisi ve yakalama kuralları için Python try except hata yönetimi yazısına bakabilirsiniz.

Generator'lar ve iş parçacıkları (Python 3.15)

Generator'lar aynı anda birden fazla iş parçacığından kullanılmak üzere tasarlanmamıştır. threading belgesine göre generator'ın bir yöntemi, generator zaten çalışırken başka bir yerden çağrılırsa ValueError yükselir; serbest iş parçacıklı (free-threaded) yapıya ilişkin belge de aynı iterator nesnesine birden fazla iş parçacığından erişmenin genel olarak güvenli olmadığını, öğelerin tekrarlanabileceğini ya da atlanabileceğini söyler.

Python 3.15 bu sorun için threading modülüne üç araç ekledi: serialize_iterator sarmalayıcısı, __next__() çağrılarını bir kilitle sıraya sokar ve her değeri tam olarak bir tüketiciye verir; synchronized_iterator bir generator fonksiyonunu dekore ederek ürettiği her generator'ı otomatik sarmalar; concurrent_tee() ise tek kaynaktan, her biri bütün değerleri gören birden çok iterator üretir. Aşağıdaki örnekte iki iş parçacığı aynı generator'ı paylaşıyor ve hiçbir değer tekrarlanmıyor.

python
import threading

def kareler(n):
    for x in range(n):
        yield x * x

kaynak = threading.serialize_iterator(kareler(6))
alinan = []

def tuket(iterable):
    for oge in iterable:
        alinan.append(oge)

t1 = threading.Thread(target=tuket, args=(kaynak,))
t2 = threading.Thread(target=tuket, args=(kaynak,))
t1.start(); t2.start()
t1.join(); t2.join()
print(sorted(alinan))

# çıktı: [0, 1, 4, 9, 16, 25]

İş parçacıkları, kilitler ve havuzlar Python threading ve multiprocessing yazısında anlatılıyor. Asenkron generator'lar (async def içinde yield) ve async for ise Python asyncio, async ve await yazısının konusu.

Sık yapılan hatalar

Sık yapılan hatalar: Generator'ı iki kez dolaşmak, len() ve indeks kullanmak, Fonksiyonun çalıştığını sanmak, Generator'ı yazdırmak
  • Generator'ı iki kez dolaşmak: İkinci dolaşma sessizce boş döner. Değerleri saklamanız gerekiyorsa list() ile listeye çevirin.
  • len() ve indeks kullanmak: Generator'ın uzunluğu önceden bilinmez; len(g) ve g[0] aşağıdaki örnekte görüldüğü gibi TypeError verir. İlk öğe için next(g) kullanın.
  • Fonksiyonun çalıştığını sanmak: uret() çağrısı gövdeyi çalıştırmaz. Generator'ı hiç tüketmezseniz içindeki kod, yan etkileri de dahil, hiç çalışmaz.
  • Generator'ı yazdırmak: print(g) değerleri değil, nesnenin temsilini gösterir. Değerleri görmek için list(g) kullanın (ama bu generator'ı tüketir).
  • raise StopIteration yazmak: PEP 479 nedeniyle RuntimeError'a dönüşür; return kullanın.
  • Sonsuz generator'ı list() ile tüketmek: Program asla dönmez; islice() veya bir break koşulu kullanın.
  • Comprehension içinde yield: Python 3.8'den beri comprehension ve generator ifadelerinin örtük kapsamında yield kullanmak yasaktır.
python
g = (x for x in range(3))
for ifade in ('len(g)', 'g[0]'):
    try:
        eval(ifade)
    except TypeError as e:
        print(type(e).__name__ + ':', e)
print(type(g).__name__, hasattr(g, '__next__'), hasattr([1], '__next__'))

# çıktı: TypeError: object of type 'generator' has no len()
# çıktı: TypeError: 'generator' object is not subscriptable
# çıktı: generator True False

Ne zaman generator, ne zaman liste?

Generator'lar her durumda listeden üstün değildir. Veriyi birden fazla kez dolaşacaksanız, uzunluğunu bilmeniz gerekiyorsa, rastgele erişim (indeks) yapacaksanız ya da veri zaten küçükse liste daha uygundur. Veri büyükse, dosya veya ağ gibi bir akıştan geliyorsa, sonsuzsa ya da hattın sonunda yalnızca bir toplam veya ilk eşleşme lazımsa generator daha uygundur. Aradaki seçimi yaparken ölçüt "bu değerleri kaç kez ve nasıl kullanacağım?" sorusudur.

Sık Sorulan Sorular

Generator ile iterator aynı şey mi?

Her generator bir iterator'dır, ama her iterator generator değildir. Generator, yield içeren bir fonksiyonun ya da bir generator ifadesinin ürettiği özel bir iterator türüdür; send(), throw() ve close() gibi ek yöntemleri vardır. Liste iterator'ı ya da sınıfla yazdığınız bir iterator da protokolü uygular ama bu ek yöntemlere sahip değildir.

Generator'ı başa sarabilir miyim?

Hayır. Iterator protokolü geri gitmeyi ya da sıfırlamayı tanımlamaz. Aynı değerleri yeniden istiyorsanız generator fonksiyonunu yeniden çağırın veya değerleri bir listeye kaydedin. Aynı akışı iki ayrı tüketiciye vermek için itertools.tee() de kullanılabilir; bu fonksiyon tüketilmeyen değerleri ara bellekte tutar.

Generator içinde return ne işe yarar?

return generator'ı bitirir. Bir değerle kullanılırsa (return sonuc), bu değer StopIteration.value olarak taşınır. for döngüsü bu değeri yok sayar; onu almanın pratik yolu sonuc = yield from alt_generator() yazımıdır.

Generator ifadesi mi, map() mi?

İkisi de tembel çalışır. map() var olan bir fonksiyonu uygulamak için kısa ve okunaklıdır; dönüşüm bir ifade ve koşul içeriyorsa generator ifadesi genellikle daha okunur. Ayrıntılar için Python lambda, map, filter ve sorted yazısına bakabilirsiniz.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Python öğrenme rehberi · List comprehension · itertools ve functools · for ve while döngüleri · asyncio, async ve await · Python decorator yazma