İçeriğe geç

Python Generators ve Iterators: Bellek Verimli Programlamanın Sırrı

T
Tolgahan
· · 14 dk okuma · 206 görüntülenme

Python Generators ve Iterators: Bellek Verimli Programlamanın Sırrı

Diyelim ki bir kütüphanedesiniz ve 10 milyon kitabın listesini almanız gerekiyor. İki seçeneğiniz var: ya tüm kitapları dev bir kamyona yükleyip evinize getirirsiniz, ya da kütüphaneden tek tek alıp okuduğunuz kitabı geri verirsiniz. İlk yöntemde evinize sığmayabilir — belki garajı da doldurursunuz, belki komşunun bahçesini de. İkinci yöntemde ise elinizde her an sadece bir kitap var ve bellek problemi diye bir şey yok.

İşte Python'daki generators ve iterators tam olarak bu ikinci yöntemi temsil ediyor. Verileri bellekte biriktirmek yerine, ihtiyaç duyulduğunda teker teker üretiyorlar. Bu özellikle büyük veri setleri, dosya okuma, API pagination ve sonsuz seriler gibi senaryolarda hayat kurtarıcı.

Peki neden bu kadar önemli? Çünkü gerçek dünya projelerinde karşılaştığınız veri miktarları, RAM'inize sığmayabilir. 5 GB'lık bir log dosyasını okumak, milyonlarca veritabanı kaydını işlemek, sürekli akan sensör verilerini analiz etmek… Bunların hepsinde klasik liste yaklaşımı ya programınızı patlatır ya da yavaşlatır. Generators bu sorunu kökünden çözer: lazy evaluation (tembel değerlendirme) prensibiyle verileri yalnızca istendiğinde üretir, bellekte tutmaz.

Bu yazıda Python'ın iteration protokolünü sıfırdan anlayacak, generator'ların nasıl çalıştığını perde arkasından görecek, gerçek projelerde nasıl kullanıldığını öğrenecek ve yaygın hatalardan nasıl kaçınacağınızı bileceksiniz.


Iteration Protokolü: Her Şeyin Temeli

Python'da bir for döngüsü yazdığınızda arka planda neler olduğunu hiç merak ettiniz mi? for item in my_list dediğinizde, Python aslında bir protokol çalıştırıyor — Iteration Protocol (yineleme protokolü). Bu protokolü anlamadan generator'ları tam olarak kavramak zor.

Protokol iki temel metoda dayanır:

  • __iter__() — Nesnenin kendisini (veya bir iterator nesnesini) döndürür. "Ben üzerinde gezinilebilir bir şeyim" demektir.

  • __next__() — Sıradaki elemanı döndürür. Eleman kalmadığında StopIteration exception'ı fırlatır.

Bir for döngüsü yazdığınızda Python şunları yapar:

  1. Nesne üzerinde iter() çağırır → bir iterator nesnesi alır

  2. Bu iterator üzerinde next() çağırır → sıradaki elemanı alır

  3. StopIteration gelene kadar 2. adımı tekrarlar

Bunu kanıtlayalım:

# for döngüsünün perde arkası
sayilar = [10, 20, 30]

# Bu for döngüsü...
for sayi in sayilar:
    print(sayi)

# ...aslında şunu yapıyor:
iterator = iter(sayilar)  # __iter__() çağrılır
while True:
    try:
        sayi = next(iterator)  # __next__() çağrılır
        print(sayi)
    except StopIteration:
        break  # Eleman bitti, döngü sona erer

# Çıktı (her ikisi için aynı):
# 10
# 20
# 30

Şimdi önemli bir ayrım: iterable ve iterator farklı kavramlar.

  • Iterable (gezinilebilir): __iter__() metodu olan her nesne. Listeler, tuple'lar, string'ler, dictionary'ler hep iterable'dır. Bunlar üzerinde birden fazla kez for döngüsü kurabilirsiniz.

  • Iterator (yineleyici): Hem __iter__() hem __next__() metodu olan nesne. Bir kez tüketilir, ikinci kez kullanamazsınız.

🎯 Analoji: Iterable bir kitaptır — istediğiniz kadar baştan okuyabilirsiniz. Iterator ise kitabın sayfalarını yırtan bir okuyucu — bir kez okudu mu, sayfalar gitti.

Kendi iterator'ımızı yazarak bunu somutlaştıralım:

class GeriSayim:
    """Verilen sayıdan 1'e kadar geri sayan iterator"""

    def __init__(self, baslangic):
        self.baslangic = baslangic
        self.mevcut = baslangic

    def __iter__(self):
        # Iterator'ın kendisini döndür
        return self

    def __next__(self):
        if self.mevcut <= 0:
            raise StopIteration  # Eleman kalmadı
        deger = self.mevcut
        self.mevcut -= 1
        return deger


# Kullanım
for sayi in GeriSayim(5):
    print(sayi, end=" ")

# Çıktı: 5 4 3 2 1

Bu kod çalışıyor ama dikkat edin: __init__, __iter__, __next__, durum yönetimi, StopIteration fırlatma… Basit bir geri sayım için bile epey kod yazdık. İşte tam bu noktada generator'lar sahneye çıkıyor.


Generators: Iterator Yazmayı Kolaylaştıran Sihir

Generator, bir iterator'ı tek satırlık bir fonksiyonla oluşturmanızı sağlayan Python özelliği. Yukarıda yazdığımız GeriSayim sınıfının tamamını şu şekilde yazabilirsiniz:

def geri_sayim(baslangic):
    """Verilen sayıdan 1'e kadar geri sayan generator"""
    mevcut = baslangic
    while mevcut > 0:
        yield mevcut  # Değeri üret ve dur
        mevcut -= 1


# Kullanım — tıpatıp aynı
for sayi in geri_sayim(5):
    print(sayi, end=" ")

# Çıktı: 5 4 3 2 1

yield anahtar kelimesi buradaki sihirli kelime. Bir fonksiyonda yield gördüğünüz an, o fonksiyon artık normal bir fonksiyon değil — bir generator fonksiyondur. Çağırdığınızda kodu çalıştırmaz, bir generator nesnesi döndürür. Bu nesne üzerinde next() çağırdığınızda kod yield'a kadar çalışır, değeri döndürür ve durur. Bir sonraki next() çağrısında kaldığı yerden devam eder.

Bunu adım adım görelim:

def basit_generator():
    print("Birinci yield öncesi")
    yield 1
    print("İkinci yield öncesi")
    yield 2
    print("Üçüncü yield öncesi")
    yield 3
    print("Fonksiyon sonu")


gen = basit_generator()
print(type(gen))
# Çıktı: <class 'generator'>

print(next(gen))
# Çıktı:
# Birinci yield öncesi
# 1

print(next(gen))
# Çıktı:
# İkinci yield öncesi
# 2

print(next(gen))
# Çıktı:
# Üçüncü yield öncesi
# 3

# Bir kez daha çağırırsak:
# next(gen)  → StopIteration fırlatır
# "Fonksiyon sonu" yazdırılır ama değer döndürülmez

Gördüğünüz gibi yield bir duraklama noktası. Fonksiyon o noktada donuyor, tüm yerel değişkenleri (local variables) bellekte korunuyor, ve next() çağrıldığında tam kaldığı yerden devam ediyor. return'den temel farkı bu: return fonksiyonu tamamen bitirir ve her şeyi siler, yield ise askıya alır.

💡 İpucu: Generator fonksiyonun içinde return da kullanabilirsiniz — bu durumda generator hemen StopIteration fırlatır ve sonlanır. return ile bir değer döndürürseniz, bu değer StopIteration exception'ının value attribute'unda saklanır ama for döngüsü bu değeri görmez.


Bellek Farkı: Liste vs Generator

Generator'ların gerçek gücü bellek yönetiminde ortaya çıkıyor. Bunu somut bir örnekle ölçelim:

import sys

# Liste yaklaşımı — tüm elemanlar bellekte
liste = [x * x for x in range(1_000_000)]
print(f"Liste boyutu: {sys.getsizeof(liste):,} bytes")
# Çıktı: Liste boyutu: 8,448,728 bytes (~8 MB)

# Generator yaklaşımı — tembel değerlendirme
generator = (x * x for x in range(1_000_000))
print(f"Generator boyutu: {sys.getsizeof(generator):,} bytes")
# Çıktı: Generator boyutu: 200 bytes

# İkisi de aynı sonucu verir
print(sum([x * x for x in range(1_000_000)]))  # Liste ile
print(sum(x * x for x in range(1_000_000)))    # Generator ile
# Her ikisi de: 333332833333500000

8 MB'a karşı 200 byte! 42.000 kat fark var. Generator bellekte sadece mevcut durumunu (hangi adımda olduğunu) tutuyor. Liste ise 1 milyon tam sayının hepsini RAM'de saklıyor.

Şimdi düşünün: 10 milyon, 100 milyon, 1 milyar eleman olsa ne olur? Liste yaklaşımında RAM tükenir ve MemoryError alırsınız. Generator ile hiç sorun olmaz — çünkü her an bellekte sadece bir eleman var.

⚠️ Dikkat: Generator expression (üretici ifade) ile list comprehension (liste üretici) sözdizimi neredeyse aynıdır. Tek fark parantez tipi: köşeli parantez [...] liste üretir, normal parantez (...) generator üretir. Bu küçük fark bellek kullanımında devasa sonuçlar yaratır.


Generator Expressions: Tek Satırlık Güç

Generator fonksiyon yazmak yerine, basit durumlar için generator expression kullanabilirsiniz. List comprehension'ın tembel (lazy) versiyonu olarak düşünebilirsiniz:

# List comprehension — hemen hesaplar, belleğe yazar
kareler_liste = [x ** 2 for x in range(10)]

# Generator expression — hesaplamayı erteler
kareler_gen = (x ** 2 for x in range(10))

# İkisini de döngüde kullanabilirsiniz
for kare in kareler_gen:
    print(kare, end=" ")
# Çıktı: 0 1 4 9 16 25 36 49 64 81

# Fonksiyonlara doğrudan geçirebilirsiniz
toplam = sum(x ** 2 for x in range(10))
print(toplam)  # 285

en_buyuk = max(x ** 2 for x in range(10))
print(en_buyuk)  # 81

# Filtreleme ile birlikte
cift_kareler = sum(x ** 2 for x in range(20) if x % 2 == 0)
print(cift_kareler)  # 1140

sum(), max(), min(), any(), all() gibi fonksiyonlara generator expression geçirmek özellikle güzel çalışır. Çünkü bu fonksiyonlar zaten elemanları teker teker tüketir — listeye ihtiyaçları yok.


yield from: Alt Generator'ları Bağlama

Python 3.3 ile gelen yield from ifadesi, bir generator'dan başka bir iterable veya generator'ı delege etmenizi sağlar. İç içe generator'larla çalışırken çok işe yarar.

def sayilar():
    yield from range(3)       # 0, 1, 2 üretir

def harfler():
    yield from "abc"           # 'a', 'b', 'c' üretir

def birlestir():
    yield from sayilar()       # Önce sayılar
    yield from harfler()       # Sonra harfler
    yield from [100, 200]      # Sonra liste


for eleman in birlestir():
    print(eleman, end=" ")
# Çıktı: 0 1 2 a b c 100 200

yield from olmadan aynı şeyi yapmak için iç döngü yazmanız gerekirdi:

# yield from kullanmadan — daha uzun, daha karmaşık
def birlestir_eski():
    for sayi in sayilar():
        yield sayi
    for harf in harfler():
        yield harf
    for eleman in [100, 200]:
        yield eleman

yield from özellikle recursive (özyinelemeli) generator'larda parlıyor. Bir dizin ağacını (directory tree) dolaşan generator düşünün:

import os

def dosya_tara(dizin):
    """Verilen dizin ve alt dizinlerdeki tüm dosyaları üretir"""
    for girdi in os.scandir(dizin):
        if girdi.is_file():
            yield girdi.path
        elif girdi.is_dir():
            yield from dosya_tara(girdi.path)  # Alt dizine dal


# Kullanım — milyonlarca dosya olsa bile bellek sabit kalır
for dosya in dosya_tara("/var/log"):
    if dosya.endswith(".log"):
        print(dosya)

Bu generator ne kadar derin bir dizin yapısı olursa olsun, bellekte sadece mevcut dosya yolunu tutar. Tüm dosya listesini bir listeye yüklemez.


Gerçek Dünya Senaryoları

Generators'ın teorisini anladık. Şimdi gerçek projelerde nerede kullanılır, onu görelim.

Senaryo 1: Büyük Dosya Okuma

Bir log dosyasını satır satır işlemek, generator'ların en klasik kullanım alanıdır:

def dosya_satir_oku(dosya_yolu, kodlama="utf-8"):
    """Büyük dosyaları bellek dostu şekilde satır satır okur"""
    with open(dosya_yolu, "r", encoding=kodlama) as dosya:
        for satir in dosya:
            yield satir.strip()


def hata_satirlari_bul(dosya_yolu):
    """Log dosyasından sadece ERROR satırlarını filtreler"""
    for satir in dosya_satir_oku(dosya_yolu):
        if "ERROR" in satir:
            yield satir


# 5 GB'lık bir log dosyasını bile işleyebilir
# Bellekte sadece 1 satır bulunur
for hata in hata_satirlari_bul("/var/log/app.log"):
    print(hata)

Bu kodda open() fonksiyonunun kendisi de aslında bir iterator döndürür — dosyayı satır satır okur. Biz üzerine kendi generator katmanımızı ekleyerek bir pipeline (boru hattı) oluşturuyoruz.

Senaryo 2: API Pagination

Bir API'den sayfalanmış (paginated) veri çekerken generator kullanmak, hem kodu temizler hem de bellek kullanımını düşürür:

import requests

def api_sayfa_getir(base_url, sayfa_boyutu=100):
    """API'den tüm sayfaları tembel olarak çeker"""
    sayfa = 1
    while True:
        yanit = requests.get(
            base_url,
            params={"page": sayfa, "per_page": sayfa_boyutu}
        )
        veriler = yanit.json()

        if not veriler:
            break  # Veri kalmadı, generator biter

        for kayit in veriler:
            yield kayit

        sayfa += 1


# Kullanım — 100.000 kayıt olsa bile bellekte sadece 1 kayıt
for urun in api_sayfa_getir("https://api.example.com/products"):
    print(f"Ürün: {urun['name']} - Fiyat: {urun['price']}")

    # İstediğiniz zaman durabilirsiniz — gereksiz API çağrısı yapılmaz
    if urun["price"] > 1000:
        print("Pahalı ürün bulundu, duruyorum.")
        break

Buradaki break çok önemli. Generator tembel olduğu için, ihtiyacınız olan veriyi bulduğunuzda döngüden çıktığınızda geriye kalan sayfalar için API çağrısı yapılmaz. Liste yaklaşımında ise tüm sayfaları çekip bitirmeniz gerekirdi.

Senaryo 3: Veri İşleme Pipeline'ı

Generator'ları birbirine bağlayarak güçlü veri işleme boru hatları oluşturabilirsiniz. Unix pipe'larına benzer bir yaklaşım:

def satir_oku(dosya_yolu):
    """1. aşama: dosyadan satır oku"""
    with open(dosya_yolu) as f:
        for satir in f:
            yield satir.strip()

def json_parse(satirlar):
    """2. aşama: her satırı JSON olarak parse et"""
    import json
    for satir in satirlar:
        if satir:
            yield json.loads(satir)

def filtrele(kayitlar, alan, deger):
    """3. aşama: belirli bir alana göre filtrele"""
    for kayit in kayitlar:
        if kayit.get(alan) == deger:
            yield kayit

def donustur(kayitlar):
    """4. aşama: veriyi dönüştür"""
    for kayit in kayitlar:
        yield {
            "ad": kayit["name"].upper(),
            "toplam": kayit["price"] * kayit["quantity"]
        }


# Pipeline'ı birleştir
satirlar = satir_oku("orders.jsonl")
kayitlar = json_parse(satirlar)
aktif_kayitlar = filtrele(kayitlar, "status", "active")
sonuclar = donustur(aktif_kayitlar)

# Tüm pipeline tembel — ilk next() çağrısına kadar hiçbir şey çalışmaz
for sonuc in sonuclar:
    print(sonuc)

Bu pipeline'ın güzelliği şu: dosya ne kadar büyük olursa olsun bellekte her an yalnızca bir kayıt var. Her aşama önceki aşamadan bir eleman alır, işler ve sonraki aşamaya geçirir. Tıpkı fabrikadaki üretim bandı gibi — tüm ürünleri depolamıyorsunuz, bant üzerinde birer birer ilerliyorlar.


Sonsuz Seriler: Bitmeyen Generator'lar

Generator'ların liste üzerindeki en belirgin avantajı sonsuz seriler üretebilmesidir. Bir listeye sonsuz eleman koyamazsınız ama bir generator sonsuza kadar değer üretebilir:

def fibonacci():
    """Sonsuz Fibonacci serisi üretir"""
    a, b = 0, 1
    while True:  # Sonsuz döngü — ama sorun yok!
        yield a
        a, b = b, a + b


# İlk 15 Fibonacci sayısı
from itertools import islice

for sayi in islice(fibonacci(), 15):
    print(sayi, end=" ")
# Çıktı: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

# 1000'den büyük ilk Fibonacci sayısı
for sayi in fibonacci():
    if sayi > 1000:
        print(f"\n1000'den büyük ilk Fibonacci: {sayi}")
        break
# Çıktı: 1000'den büyük ilk Fibonacci: 1597

itertools.islice() fonksiyonu generator'larla çalışırken çok kullanışlı. Normal dilim (slice) operatörü [:] generator'larda çalışmaz çünkü generator indekslenebilir (indexable) değildir. islice() bu sorunu çözer.

⚠️ Dikkat: Sonsuz bir generator üzerinde list(), len(), sum() gibi tüm elemanları tüketmeye çalışan fonksiyonları çağırmayın. Program sonsuza kadar çalışır veya bellek tükenir. Her zaman islice(), break veya takewhile() ile sınırlayın.


Generator'lara Veri Göndermek: send() Metodu

Generator'lar sadece veri üretmez, dışarıdan veri de alabilir. send() metodu ile generator'a çalışma anında veri gönderebilirsiniz:

def hareketli_ortalama():
    """Gelen sayıların hareketli ortalamasını hesaplar"""
    toplam = 0
    adet = 0
    ortalama = None

    while True:
        deger = yield ortalama  # Dışarıdan değer al, ortalamayı döndür
        if deger is not None:
            toplam += deger
            adet += 1
            ortalama = toplam / adet


# Kullanım
ort = hareketli_ortalama()
next(ort)  # Generator'ı başlat (ilk yield'a kadar çalıştır)

print(ort.send(10))   # Çıktı: 10.0
print(ort.send(20))   # Çıktı: 15.0
print(ort.send(30))   # Çıktı: 20.0
print(ort.send(40))   # Çıktı: 25.0

send() kullandığınızda yield ifadesi iki yönlü çalışır: hem değer döndürür hem de değer alır. deger = yield ortalama satırında, ortalama dışarıya döner, send() ile gönderilen değer ise deger'e atanır.

💡 İpucu: send() kullanmadan önce generator'ı next() ile başlatmanız gerekir. Çünkü generator ilk yield'a kadar ilerlemeli ki send() ile gönderdiğiniz değeri alabilsin. Bu adımı atlarsanız TypeError: can't send non-None value to a just-started generator hatası alırsınız.


itertools ile Generator Gücünü Katlamak

Python'un itertools modülü, generator'larla çalışırken kullanabileceğiniz hazır araçlar sunar. Hepsi tembel çalışır, yani bellek dostu:

from itertools import chain, islice, takewhile, dropwhile, count, cycle, repeat

# chain — birden fazla iterable'ı birleştirir
birlesik = chain([1, 2, 3], [4, 5, 6], [7, 8, 9])
print(list(birlesik))
# Çıktı: [1, 2, 3, 4, 5, 6, 7, 8, 9]

# count — sonsuz sayaç
# count(10, 2) → 10, 12, 14, 16, 18, ...
ilk_bes = list(islice(count(10, 2), 5))
print(ilk_bes)
# Çıktı: [10, 12, 14, 16, 18]

# cycle — iterable'ı sonsuz tekrarlar
# cycle("ABC") → A, B, C, A, B, C, A, ...
tekrar = list(islice(cycle("ABC"), 8))
print(tekrar)
# Çıktı: ['A', 'B', 'C', 'A', 'B', 'C', 'A', 'B']

# takewhile — koşul sağlandığı sürece al
sayilar = [2, 4, 6, 8, 1, 3, 5, 7]
ciftler = list(takewhile(lambda x: x % 2 == 0, sayilar))
print(ciftler)
# Çıktı: [2, 4, 6, 8]  (ilk tek sayıda durur)

# dropwhile — koşul sağlandığı sürece atla, sonra hepsini al
geri_kalan = list(dropwhile(lambda x: x % 2 == 0, sayilar))
print(geri_kalan)
# Çıktı: [1, 3, 5, 7]  (ilk tek sayıdan itibaren alır)

Bu araçları kendi generator'larınızla birleştirerek çok güçlü veri işleme akışları kurabilirsiniz. Hiçbiri belleğe yüklemez, hepsi tembel çalışır.


Yaygın Hatalar ve Tuzaklar

Generator'larla çalışırken herkesin düştüğü tuzaklar var. Bunları bilmek sizi saatlerce debug'dan kurtarır.

Tuzak 1: Generator'ı İki Kez Tüketmeye Çalışmak

Bu en yaygın hata. Generator bir kez tüketilir, ikinci sefer boş döner:

def sayilar_uret():
    yield 1
    yield 2
    yield 3

gen = sayilar_uret()

# İlk kullanım — çalışır
print(list(gen))  # [1, 2, 3]

# İkinci kullanım — BOŞ!
print(list(gen))  # []

# Çözüm 1: Yeni generator oluşturun
gen2 = sayilar_uret()
print(list(gen2))  # [1, 2, 3]

# Çözüm 2: itertools.tee ile kopyalayın (dikkatli kullanın)
from itertools import tee
gen_a, gen_b = tee(sayilar_uret(), 2)
print(list(gen_a))  # [1, 2, 3]
print(list(gen_b))  # [1, 2, 3]

⚠️ Dikkat: itertools.tee() kullanırken dikkatli olun. Eğer bir kopya diğerinden çok ilerdeyse, aradaki tüm elemanlar bellekte tutulur. Büyük veri setlerinde tee() bellek avantajınızı sıfırlayabilir.

Tuzak 2: Generator'da len() Kullanmaya Çalışmak

Generator'ların uzunluğu yoktur çünkü elemanlar henüz üretilmemiştir:

gen = (x for x in range(100))

# Bu HATA verir:
# len(gen)  → TypeError: object of type 'generator' has no len()

# Eleman sayısını öğrenmek istiyorsanız tüketmeniz gerekir:
adet = sum(1 for _ in gen)  # 100 — ama artık gen tükenmiştir!

Tuzak 3: Generator İçinde Scope Sorunu

Bu tuzak özellikle döngü içinde generator oluşturanlarda sık görülür:

# YANLIŞ — tüm generator'lar aynı 'i' değerine bağlı
generators_yanlis = [
    (x * i for x in range(3))
    for i in range(4)
]

# i son değeri olan 3'ü kullanır — sürpriz!
for gen in generators_yanlis:
    print(list(gen))
# Beklenmeyen çıktı — tüm generator'lar i=3 ile çalışır

# DOĞRU — lambda ile kapama (closure) oluşturun
def carpan_generator(carpan):
    return (x * carpan for x in range(3))

generators_dogru = [carpan_generator(i) for i in range(4)]
for gen in generators_dogru:
    print(list(gen))
# Çıktı:
# [0, 0, 0]
# [0, 1, 2]
# [0, 2, 4]
# [0, 3, 6]

Tuzak 4: Generator İçinde return ile Değer Kaybetmek

def hatali_generator():
    yield 1
    yield 2
    return 3     # Bu değer for döngüsünde KAYBOLUR
    yield 4      # Bu satır asla çalışmaz

for deger in hatali_generator():
    print(deger)
# Çıktı: 1 2
# 3 ve 4 asla yazdırılmaz!

return generator'ı sonlandırır ve StopIteration fırlatır. return 3 deki 3 değeri StopIteration exception'ının value attribute'una atanır ama for döngüsü bunu yakalar ve görmezden gelir. return'den sonraki yield'lar ise hiçbir zaman ulaşılamaz (unreachable code).


Best Practices: Profesyonel Generator Kullanımı

1. Büyük Veri → Generator, Küçük Veri → Liste

Eğer veri seti RAM'e rahatça sığıyorsa ve birden fazla kez erişmeniz gerekiyorsa, liste kullanın. Generator her durumda doğru tercih değil:

# Küçük veri — liste daha pratik
kullanicilar = [kullanici for kullanici in veritabani.getir_hepsi()]  # 50 kayıt

# Büyük veri — generator şart
log_satirlari = (satir for satir in dosya_oku("10gb_log.txt"))

2. Generator Pipeline'larını Modüler Tutun

Her aşama tek bir iş yapmalı. Küçük, birleştirilebilir generator'lar yazın:

# İyi — her fonksiyon tek bir iş yapıyor
def oku(kaynak):
    ...

def filtrele(veriler, kosul):
    ...

def donustur(veriler, islem):
    ...

# Birleştir
sonuc = donustur(filtrele(oku("data.csv"), kosul), islem)

3. Generator'ı Fonksiyona Geçirirken Paranteze Dikkat

# Fonksiyonun tek argümanı generator ise ekstra parantez gereksiz
toplam = sum(x ** 2 for x in range(10))  # Doğru ve temiz

# Birden fazla argüman varsa parantez gerekir
sonuc = max((x ** 2 for x in range(10)), default=0)

4. close() ile Kaynakları Temizleyin

Generator'ınız dosya, ağ bağlantısı gibi kaynaklar kullanıyorsa, try/finally veya close() ile temizleme yapın:

def veritabani_oku(sorgu):
    baglanti = veritabani_baglan()
    try:
        cursor = baglanti.execute(sorgu)
        for satir in cursor:
            yield satir
    finally:
        baglanti.kapat()  # Generator tüketilsin veya tüketilmesin, kapanır

5. Tip Belirteci (Type Hint) Kullanın

Python 3.9+ ile generator'lara tip belirteci ekleyebilirsiniz:

from collections.abc import Generator

def cift_sayilar(limit: int) -> Generator[int, None, None]:
    """Çift sayılar üreten generator
    Generator[YieldType, SendType, ReturnType]
    """
    for i in range(0, limit, 2):
        yield i

Ne Zaman Generator, Ne Zaman Liste?

DurumTercihNeden
Veri RAM'e sığmıyorGeneratorBellek tasarrufu
Veriye birden fazla kez erişim gerekiyorListeGenerator tek kullanımlık
Sonsuz seri / sürekli akan veriGeneratorListe sonsuz olamaz
Veri pipeline'ı (oku → filtrele → dönüştür)GeneratorHer aşama tembel çalışır
Elemanların indeksle erişimi gerekiyorListeGenerator indekslenemez
Veri setinin uzunluğunu bilmek gerekiyorListeGenerator'da len() yok
API pagination / dosya okumaGeneratorGereksiz yükleme yapmaz

Özet

  • Iterator, __iter__() ve __next__() metodlarını implemente eden nesnedir. Python'daki for döngüsü bu protokolü kullanır.

  • Generator fonksiyon, yield anahtar kelimesi içeren fonksiyondur. Çağrıldığında iterator döndürür, kodu çalıştırmaz. Her next() çağrısında yield'a kadar çalışır ve durur.

  • Generator expression, tek satırlık generator oluşturma yöntemidir. (x for x in range(n)) şeklinde yazılır ve list comprehension'ın tembel versiyonudur.

  • Bellek avantajı devasa: 1 milyon elemanlı listede ~8 MB, aynı generator'da ~200 byte. Büyük veri setlerinde generator kullanmak zorunluluktur, tercih değil.

  • Tek kullanımlık oldukları en kritik kural: bir generator tüketildikten sonra tekrar kullanılamaz. Birden fazla geçiş gerekiyorsa yeni generator oluşturulmalı veya listeye dönüştürülmeli.

  • Pipeline'lar generator'ların en güçlü kullanım alanı: oku → filtrele → dönüştür → yaz akışında her aşama tembel çalışır, bellekte her an tek eleman bulunur.

  • yield from ile alt generator'ları delege edebilir, send() ile generator'a çalışma anında veri gönderebilir, itertools modülü ile hazır tembel araçları kullanabilirsiniz.

Paylaş:
Son güncelleme: Apr 21, 2026

Yorumlar

Giriş yapın ve yorum bırakın.

Henüz yorum yok

Düşüncelerinizi paylaşan ilk siz olun!

Bu yazıyı beğendiniz mi?

Bültene abone olun ve yeni yazılardan ilk siz haberdar olun. Spam yok, söz.

İlgili Yazılar