Nesne Tabanlı Programlama 2

8 - Python’ın İleri Seviye Özellikleri II – Iteratorlar, Üreteçler ve Tip İpuçları

Emre Can Yılmaz

Ondokuz Mayıs Üniversitesi

2026

Bu Hafta

Önceki haftalarda Python’da özel metotları, nesne davranışlarını ve fonksiyonların da birer nesne gibi ele alınabildiğini gördük.

Bu hafta şu üç soruya odaklanıyoruz:

  • for döngüsü perde arkasında gerçekten ne yapıyor?
  • Her veriyi neden baştan listeye doldurmak istemeyiz?
  • Bir fonksiyonun hangi tür veri beklediğini daha açık nasıl yazarız?

Bugünkü başlıklar ilk bakışta farklı görünebilir. Ama aslında aynı fikre bağlanırlar:

Bazı durumlarda veriye ya da sonuca bir anda değil, adım adım ulaşırız.

Bu çerçevede şunları ayırt edebiliyor olmanızı hedefliyoruz:

  • iterable, iterator ve generator kavramlarını
  • for yapısının iter() ve next() mantığıyla nasıl çalıştığını
  • Sınıf tabanlı iterator ile yield kullanan üreteç arasındaki farkı
  • Tip ipuçlarının ne sağladığını ve ne sağlamadığını

Yol Haritası

Bugünkü akış üç parçadan oluşuyor:

  1. İterasyon
    • for döngüsü nasıl çalışır?
    • iterable ve iterator ilişkisi nedir?
  2. Üreteçler
    • yield ne yapar?
    • Değerleri neden ihtiyaç oldukça üretmek isteriz?
  3. Tip İpuçları
    • Kodun hangi tür veri beklediğini imzada nasıl gösteririz?
    • Iterable[int] ile Iterator[int] arasında ne fark vardır?

Ders boyunca şu soruya tekrar döneceğiz:

Veri elimde hazır mı, yoksa değerler tek tek mi geliyor?

for Döngüsü Perde Arkasında Ne Yapar?

my_list = [10, 20, 30]
it = iter(my_list)

print(next(it))
print(next(it))
print(next(it))

Bu örnek şunu gösterir:

  • Liste doğrudan sıradaki elemanı vermez
  • Önce iter(...) ile bir iterator alınır
  • Sonra elemanlar next(...) ile tek tek istenir

Yani for döngüsünü anlamak için önce bu küçük mekanizmayı anlamamız gerekir.

Iterator Protokolü

Bir nesnenin for içinde çalışabilmesi için Python şu beklentiye sahiptir:

  1. iter(nesne) çağrılabilmeli
  2. Elde edilen nesneye next(...) uygulanabilmeli

Akış kabaca şöyledir:

  • for başlarken Python iter(...) çağırır
  • Her turda next(...) ile sıradaki elemanı ister
  • Eleman kalmadığında StopIteration oluşur
  • for bu sinyali yakalar ve döngüyü bitirir

Buradaki önemli nokta şudur: for sihirli bir yapı değildir. Belirli bir kurala göre çalışır.

for Döngüsünü Elle Kuralım

my_list = [10, 20, 30]

iterator_obj = iter(my_list)
print(f"Iterator tipi: {type(iterator_obj)}")

print(f"İlk eleman: {next(iterator_obj)}")
print(f"İkinci eleman: {next(iterator_obj)}")
print(f"Üçüncü eleman: {next(iterator_obj)}")

try:
    print(next(iterator_obj))
except StopIteration:
    print("Liste bitti, StopIteration alındı.")

Bu örnek, for döngüsünün yaptığı işi daha görünür hale getirir:

  • Bir iterator alınır
  • Sıradaki değer istenir
  • Elemanlar bitince işlem sona erer

İki Terimi Netleştirelim

Burada iki farklı kavram var:

  • Iterable: Üzerinde dolaşılabilen yapı
  • Iterator: Sıradaki elemanı tek tek veren nesne

Kısa örnek:

my_list = [10, 20, 30]
it = iter(my_list)

Burada:

  • my_list bir iterable’dır
  • it ise bir iterator’dır

Yani liste veriyi tutar. Iterator ise o veriyi sırayla vermeyi sağlar.

Kendi Iterator Sınıfımızı Yazalım

class AdimliSayici:
    def __init__(self, baslangic, son, adim):
        if adim <= 0:
            raise ValueError("Bu örnek için adim pozitif ve sifirdan buyuk olmali")
        self.mevcut = baslangic
        self.son = son
        self.adim = adim

    def __iter__(self):
        return self

    def __next__(self):
        if self.mevcut >= self.son:
            raise StopIteration
        deger = self.mevcut
        self.mevcut += self.adim
        return deger

for sayi in AdimliSayici(0, 10, 2):
    print(sayi)

Bu örnekte nesnenin kendisi hem üzerinde dolaşılan yapı gibi hem de sıradaki elemanı veren nesne gibi davranıyor.

Not:

  • Bu sürüm yalnızca pozitif adım için düşünülmüştür
  • Negatif adım desteklenecekse durma koşulu ayrıca tasarlanmalıdır

Bu Tasarımın Artısı ve Eksisi Nedir?

Az önceki sınıfta nesnenin kendisi hem veriyi tutan yapı gibi hem de sıradaki elemanı veren yapı gibi davrandı.

Bu yaklaşımın artıları:

  • Yazımı doğrudandır
  • Ek bir nesne oluşturmaya gerek kalmaz
  • Küçük örneklerde öğretici olur

Bu yaklaşımın eksileri:

  • Aynı nesne yeniden baştan dolaşılamaz
  • İç durum değiştiği için ikinci kullanım ilk kullanım gibi davranmaz
  • Veriyi tutma işi ile elemanları sırayla verme işi aynı yerde toplanmış olur

Daha sonra istersek şu tasarımı da kurabiliriz:

  • Dıştaki nesne yalnızca veriyi tutar
  • __iter__() her çağrıldığında yeni bir iterator döndürür

Böylece aynı veri üzerinde yeniden dolaşmak daha doğal hale gelir.

yield Nedir?

yield, bir fonksiyonun değer üretip hemen tamamen bitmemesini sağlar.

Normalde bir fonksiyon return ile sonucu verip biter.
Ama yield kullanıldığında fonksiyon:

  • Bir değer üretir
  • O noktada durur
  • Sonraki çağrıda kaldığı yerden devam eder

Bu yüzden yield kullanan fonksiyonlara çoğu zaman generator fonksiyonu denir.

Aynı Fikri yield ile Kurmak

def adim_say(baslangic, son, adim):
    if adim <= 0:
        raise ValueError("Bu örnek için adim pozitif ve sifirdan buyuk olmali")
    mevcut = baslangic
    while mevcut < son:
        yield mevcut
        mevcut += adim

for sayi in adim_say(0, 10, 2):
    print(sayi)

Burada sınıf yazmadan da sırayla değer üretebildik.

İlk örneğe göre fark şudur:

  • İç durumu sınıfta tutmadık
  • __iter__ ve __next__ metotlarını kendimiz yazmadık
  • Daha kısa bir yolla aynı davranışa ulaştık

yield Nasıl Düşünülmeli?

Generator fonksiyonunu şu şekilde düşünebilirsiniz:

  1. Fonksiyon çağrılır ama hemen tamamen çalışmaz
  2. Bir generator nesnesi döner
  3. next() geldiğinde kod ilk yield noktasına kadar ilerler
  4. Değer dışarı verilir ve fonksiyon o noktada durur
  5. Sonraki next() çağrısında kaldığı yerden devam eder

Kısaca akış şöyledir:

Generator oluştur -> next() -> yield -> durakla -> next() -> devam et

Buradaki temel fark, fonksiyonun her değeri baştan üretmemesi; gerektiği anda üretmesidir.

yield Çalışırken Ne Olur?

def basit_sayac_uretici(ust_limit):
    print(">>> Fonksiyon başladı")
    n = 0
    while n < ust_limit:
        print(f">>> yield {n} öncesi")
        yield n
        print(f">>> yield {n} sonrası")
        n += 1
    print(">>> Fonksiyon bitti")

generator_obj = basit_sayac_uretici(2)

print(next(generator_obj))
print(next(generator_obj))

try:
    print(next(generator_obj))
except StopIteration:
    print("Uretec bitti")

Bu örnek, generator fonksiyonunun tek parça halinde değil, parça parça çalıştığını açıkça gösterir.

Neden yield Kullanıyoruz?

Bazı durumlarda tüm veriyi baştan üretmek istemeyiz:

  • Milyonlarca satırlık dosyalar
  • Sürekli gelen sensör verileri
  • Çok büyük sorgu sonuçları
  • Tek seferde belleğe sığmayacak veriler
# numbers = list(range(1_000_000_000))
# squares = [n * n for n in numbers]

Bu yaklaşım:

  • Çok fazla bellek tüketebilir
  • Gereksiz bekleme süresi doğurabilir
  • Bazı durumlarda doğrudan başarısız olabilir

Bu yüzden bazen sonuçları baştan saklamak yerine, ihtiyaç oldukça üretmek daha doğru olur.

return ile yield Aynı Şey Değildir

return yield
Fonksiyonu bitirir Fonksiyonu duraklatır
Tek bir sonuç döndürür Değerleri sırayla üretebilir
Kaldığı yeri korumaz Kaldığı yeri korur

Bu yüzden generator fonksiyonu, liste döndüren sıradan bir fonksiyonla aynı şey değildir.

Tüketilen Generator Geri Gelmez

def sayi_uret(n):
    for i in range(n):
        yield i

generator_obj = sayi_uret(3)

print(list(generator_obj))
print(list(generator_obj))

Çıktı:

[0, 1, 2]
[]

Çünkü generator’lar ve genel olarak iterator’lar çoğu zaman tek tek ilerleyen ve kullanıldıkça sona yaklaşan yapılar gibi davranır.

Aynı veriyi yeniden dolaşmak istiyorsanız:

  • Ya generator’ı yeniden oluşturmalısınız
  • Ya da veriyi baştan saklayan bir yapı kullanmalısınız

Üç Kavramı Yerine Oturtalım

Kavram Ne yapar? Örnek
Iterable Üzerinde dolaşılabilen veri kaynağıdır list, str, dict, set, dosya
Iterator Sıradaki elemanı tek tek verir iter([10, 20, 30])
Generator yield ile değer üreten özel iterator’dır def sayac(): yield 1

Kritik noktalar:

  • Her generator bir iterator’dır
  • Her iterator bir iterable gibi davranabilir
  • Ama her iterable doğrudan iterator değildir

Örneğin bir listeye doğrudan next() uygulayamazsınız; önce iter(...) ile iterator almanız gerekir.

Iterator mı, Generator mı?

İkisi de aynı kurala uyar. Fark daha çok yazım biçimi ve kullanım amacındadır.

  • İç durumu ayrıntılı kontrol etmek istiyorsanız sınıf tabanlı iterator yazabilirsiniz
  • Sadece sırayla değer üretmek istiyorsanız generator genelde daha kısa olur
  • Okunabilirlik önemliyse generator çoğu durumda daha sade bir çözüm sunar

Pratikte çoğu değer üretme işinde önce generator düşünmek iyi bir başlangıçtır.

Küçük bir not:

  • Bir generator içinden başka bir iterable ya da generator’ın değerlerini aktarmanın daha kısa bir yolu da vardır
  • İleride bunun için yield from yapısını görebilirsiniz

Üreteç İfadeleri: (...)

kareler_liste = [x * x for x in range(1000)]
kareler_uretec = (x * x for x in range(1000))

print(kareler_uretec)

for i in range(5):
    print(next(kareler_uretec))

Buradaki fark şudur:

  • [] kullanırsanız sonuçlar baştan listeye yazılır
  • () kullanırsanız yalnızca bir generator nesnesi oluşur
  • Değerler ancak istendiğinde üretilir

Bu yapı, kısa ve okunaklı biçimde tembel üretim yapmayı sağlar.

Tip İpuçları

Buraya kadar tip ipuçlarını özellikle kullanmadık.
Çünkü önce davranışı anlamak istedik.

Tip ipuçları, kodun hangi tür veriyle çalışmasının beklendiğini görünür hale getirir.

Ne sağlar?

  • Okunabilirliği artırır
  • IDE desteğini iyileştirir
  • Mypy gibi araçlarla hataları daha erken yakalamayı sağlar
  • Kodun niyetini daha açık hale getirir

Ne sağlamaz?

  • Python’ı statik tipli bir dile dönüştürmez
  • Tek başına çalışma zamanında zorunlu kontrol yapmaz

Yani tip ipucu, doğrudan davranışı değiştirmez; ama kodun sözleşmesini görünür kılar.

Bu Konuda Tip İpuçlarını Nasıl Okuyacağız?

Bu hafta açısından en önemli ayrım şudur:

  • Iterable[int]: Üzerinde dolaşılabilen kaynak
  • Iterator[int]: Sıradaki int değerini veren nesne

Generator fonksiyonları da çoğu durumda Iterator[T] döndürür.

from typing import Iterable, Iterator

def toplam_hesapla(sayilar: Iterable[int]) -> int:
    return sum(sayilar)

def harfleri_ver(kelime: str) -> Iterator[str]:
    for harf in kelime:
        yield harf

Burada ilk fonksiyon, listeye özel davranmıyor; üzerinde dolaşılabilen herhangi bir kaynağı kabul ediyor.

Ne Zaman Iterable, Ne Zaman Iterator?

Bu ayrım pratikte önemlidir.

Iterable[T] kullanmak daha uygundur, eğer:

  • Fonksiyonun tek beklentisi üzerinde dolaşılabilmesi ise
  • Liste, küme, dosya, generator gibi farklı kaynakları kabul etmek istiyorsanız
  • Arayüzü daha genel tutmak istiyorsanız

Iterator[T] kullanmak daha uygundur, eğer:

  • Elinizde gerçekten sıradaki değeri veren bir nesne varsa
  • next(...) mantığı bu nesne üzerinde anlamlıysa
  • Elemanların tek tek alınmasını özellikle vurgulamak istiyorsanız

Yani çoğu durumda parametrede daha genel olan Iterable[T], dönüşte ise gerçekten sırayla değer üretiyorsanız Iterator[T] daha doğal bir seçimdir.

Aynı Fonksiyon, Farklı Iterable Kaynaklar

from typing import Iterable

def toplam_hesapla(sayilar: Iterable[int]) -> int:
    return sum(sayilar)

liste_verisi = [1, 2, 3]
kume_verisi = {4, 5, 6}
uretec_verisi = (i for i in range(7, 10))

print(toplam_hesapla(liste_verisi))
print(toplam_hesapla(kume_verisi))
print(toplam_hesapla(uretec_verisi))

Bu örnekte ortak nokta şudur:

  • Fonksiyon yalnızca list istemiyor
  • Üzerinde dolaşılabilen herhangi bir kaynağı kabul ediyor
  • İmzadaki Iterable[int] ifadesi de tam olarak bunu anlatıyor

Küçük bir not:

  • typing.Generator[...] gibi daha ayrıntılı tip gösterimleri de vardır
  • Ama bu derste temel ayrımı net kurmak bizim için daha önemlidir

Tip İpucu Varsa Hata Otomatik Yakalanır mı?

def bir_yas_artir(yas: int) -> int:
    return yas + 1

bir_yas_artir("20")

Bu imza şunu söyler:

  • Fonksiyon int bekliyor
  • Dönüşte int üretmeyi amaçlıyor

Ama Python yorumlayıcısı sırf tip ipucu var diye çağrıyı otomatik durdurmaz.

Bu tür hataları erken fark etmek için genelde şunlar kullanılır:

  • IDE uyarıları
  • Pylance
  • Mypy

Örnek: Çift Sayıların Kareleri

from typing import Iterable, Iterator

def cift_kareleri(sayilar: Iterable[int]) -> Iterator[int]:
    for sayi in sayilar:
        if sayi % 2 == 0:
            yield sayi * sayi

rakamlar = [1, 2, 3, 4, 5, 6]

for kare in cift_kareleri(rakamlar):
    print(kare)

Burada dönüş tipi neden Iterator[int]?

  • Çünkü fonksiyon bir liste döndürmüyor
  • Sırayla int üreten bir yapı döndürüyor

Parametrede Iterable[int] kullanılması da önemlidir; çünkü bu fonksiyon yalnızca listeyle sınırlı değildir.

Özet

  • iterable, iterator ve generator farklı kavramlardır
  • for, arka planda iter() ve next() mantığıyla çalışır
  • yield, fonksiyonun değer üretip kaldığı yerden devam etmesini sağlar
  • Generator’lar değerleri baştan saklamak yerine ihtiyaç oldukça üretir
  • Iterator ve generator yapılarında değerler genellikle tek tek alınır; kullanıldıkça da sona yaklaşılır
  • Tip ipuçları kodun sözleşmesini görünür kılar, fakat tek başına çalışma zamanı denetimi yapmaz

Bugünkü ortak fikir şuydu:

Her şeyi baştan üretmek zorunda değiliz; bazen doğru yaklaşım, adım adım ilerlemektir.

Alıştırmalar

Aşağıdaki çalışmalarda üç şeyi birlikte düşünün:

  • Veri elimde hazır mı, yoksa değerler tek tek mi geliyor?
  • Bu veriyi baştan depolamak gerekiyor mu?
  • Bu fonksiyonun hangi tür veri beklediğini belirtmek gerekli mi?

Alıştırma 1: Adım Sayacı Üreteci

İstenen davranış:

  • adim_say(baslangic, son, adim) fonksiyonu yield kullanmalı
  • Değerler baslangictan başlayıp son değerine gelmeden bitmeli
  • Örnek çıktı: list(adim_say(1, 10, 2)) -> [1, 3, 5, 7, 9]
def adim_say(baslangic, son, adim):
    # burada kodunuzu yazın
    ...

Alıştırma 2: Sesli Harf Iterator’ı

İstenen davranış:

  • Sınıf bir metin almalı
  • Sadece sesli harfleri sırayla döndürmeli
  • Büyük/küçük harf farkı olmamalı

İpucu:

  • Sesli olmayan karakterlerde index değerini artırıp aramaya devam edin
  • Metnin sonuna gelirseniz StopIteration üretin

Örnek çıktı:

  • list(SesliHarfIterator("Merhaba Dünya")) -> ['e', 'a', 'a', 'ü', 'a']
class SesliHarfIterator:
    def __init__(self, metin):
        self.metin = metin
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        sesliler = "aeıioöuüAEIİOÖUÜ"
        # burada kodunuzu yazın
        ...

Alıştırma 3: Kelime Uzunlukları

İstenen davranış:

  • Verilen listedeki sadece 5 karakterden uzun kelimeleri dikkate alın
  • Bu kelimelerin kendisini değil, uzunluklarını üretin
  • Çözümde generator expression kullanın

Örnek çıktı:

  • Verilen liste için çıktı: [6, 11]
kelimeler = ["Python", "programlama", "çok", "güzel", "bir", "dil"]

uzunluklar = (
    # burada generator expression yazın
)

for uzunluk in uzunluklar:
    print(uzunluk)

Kapanış Soruları

Bir problemi çözerken şu üç soruyu düşünün:

  1. Veri elimde hazır mı, yoksa değerler tek tek mi geliyor?
  2. Bu veriyi baştan depolamak gerekiyor mu?
  3. Bu fonksiyonun hangi tür veriyle çalıştığını açıkça belirtmek gerekli mi?

Bu üç soru, çoğu zaman doğru aracı seçmenizi kolaylaştırır.

Teşekkürler