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

Motivasyon: Bu Konu Neden Var?

Önceki haftalarda sınıflara davranış kazandırdık, özel metotlarla Python’ın beklentilerini gördük ve fonksiyonların da birer nesne gibi ele alınabildiğini konuştuk.

Bu hafta şu üç soruya odaklanıyoruz:

  • for döngüsü perde arkasında gerçekten ne yapıyor?
  • Büyük veriyi neden listeye doldurmak yerine parça parça üretmek isteriz?
  • Kodun hangi tür veri beklediğini daha açık nasıl ifade ederiz?

Bu üç soru birlikte düşünülünce konu sadece sözdizimi değil, aynı zamanda doğru model kurma meselesidir.

Bu Dersin Öğrenme Hedefleri

Bu dersin sonunda şunları ayırt edebiliyor olmanızı hedefliyoruz:

  • iterable, iterator ve generator kavramları arasındaki farkı
  • for döngüsünün neden iter() ve next() mantığıyla çalıştığını
  • sınıf tabanlı iterator ile yield kullanan üreteç arasında ne fark olduğunu
  • üreteçlerin neden tek kullanımlık olduğunu
  • tip ipuçlarının ne sağladığını ve ne sağlamadığını
  • bu araçları hangi durumda tercih etmenin daha anlamlı olduğunu

Büyük Resim

Geçen haftalardaki iki fikri burada birleştiriyoruz:

  • Python bazı davranışları özel metotlarla bekler
  • fonksiyonlar da birer nesne gibi davranabilir

Bugün aslında tek bir fikir etrafında dönüyoruz:

Bir veriye ya da hesaplamaya tamamını üretmeden, adım adım erişmek.

Bu fikrin üç görünümü var:

  • koleksiyon üzerinde gezinmek: iterable
  • sıradaki elemanı istemek: iterator
  • değeri ihtiyaç anında üretmek: generator

Ardından bu akışı tip ipuçlarıyla daha açık hale getireceğiz.

İlk Somut Soru: for Gerçekte Ne Yapıyor?

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

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

Bu kısa örnek şunu gösterir:

  • liste doğrudan sıradaki elemanı vermez
  • önce 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.

İterasyon: Koleksiyonlarda Gezinmek

Az önce iter(...) ve next(...) ile küçük mekanizmayı gördük.

Şimdi bu fikri for döngüsüne genelleyelim:

  • for, koleksiyonun tüm elemanlarını sırayla ister
  • bunu yaparken doğrudan koleksiyonla değil, onun iterator’ı ile çalışır
  • yani gördüğümüz küçük örnek, aslında for döngüsünün perde arkasına açılan kapıdır

İşte burada Iterator Protokolü devreye giriyor.

Üç Kavramı Karıştırmayalım

Kavram Ne yapar? Örnek
iterable Üzerinde dolaşılabilir 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 nokta:

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

Örneğin bir listeyi doğrudan next() ile tüketemezsiniz; önce iter(...) ile iterator almanız gerekir.

Iterator Protokolü: Adım Adım Erişim Mekanizması 🚶‍♂️

  • Iterable (Yinelenebilir): İçindeki elemanlar üzerinden geçilebilen veri kaynağıdır. Liste, string, sözlük, dosya gibi yapılar bu gruptadır.
  • Iterator (Yineleyici): Bir veri kaynağından sıradaki elemanı tek tek verir ve nerede kaldığını hatırlar.

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

Teknik olarak burada olan şey şudur:

  • next(iterator_nesnesi) çağrılır
  • bu çağrı nesnenin __next__() metodunu tetikler

for Döngüsünün Çalışma Adımları:

  1. for döngüsü başlarken, iter(my_list) çağrılır. Bu, listeden bir iterator (yer imi) nesnesi alır.
  2. Döngünün her adımında, next(iterator_nesnesi) çağrılır. Bu çağrı sıradaki elemanı döndürür ve iterator’ı bir sonraki elemana ilerletir.
  3. Tüm elemanlar bitince, next(iterator_nesnesi) çağrısı özel bir sinyal olan StopIteration hatasını üretir.
  4. for döngüsü bu StopIteration sinyalini yakalar ve döngüyü sonlandırır.

Manuel Iterator Kullanımı

for döngüsünün yaptığını elle yapalım:

my_list = [10, 20, 30]

# 1. Iterable'dan iterator al (Yer imini oluştur)
iterator_obj = iter(my_list)
print(f"Iterator tipi: {type(iterator_obj)}")

# 2. Sıradaki elemanı iste (Yer iminden oku ve ilerlet)
print(f"İlk eleman: {next(iterator_obj)}") # 10
print(f"İkinci eleman: {next(iterator_obj)}") # 20
print(f"Üçüncü eleman: {next(iterator_obj)}") # 30

# 3. Başka eleman kalmadı mı? (Kitap bitti mi?)
try:
  print(next(iterator_obj))
except StopIteration:
  print("Liste bitti, StopIteration alındı.")

Bu protokol, Python’da farklı veri yapılarında tutarlı bir şekilde gezinmeyi sağlar.

Özel Iterator Sınıfı Oluşturmak

Kendi veri yapılarımız veya özel sıralı erişim mantığımız için iterator oluşturabiliriz. Örneğin, belirli bir adıma göre artan sayılar üreten bir iterator:

class AdimliSayici:
  def __init__(self, baslangic: int, son: int, adim: int) -> None:
    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) -> "AdimliSayici":
    return self

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

# Kullanım
for sayi in AdimliSayici(0, 10, 2):
  print(sayi)

Bu örnekte önemli bir ayrıntı var:

Bu nesnenin kendisi hem veri kaynağı hem iterator olduğu için tek kullanımlık davranır.

Yani aynı nesne üzerinde ikinci kez dolaşmak isterseniz başa dönmez; yeni nesne üretmeniz gerekir.

Not:

  • bu örnek yalnızca pozitif ve sıfır olmayan adım için düşünülmüştür
  • negatif adım desteklenecekse durma koşulunun ayrıca tasarlanması gerekir

Aynı İşi Üreteçle Daha Kısa Yazmak

Aynı mantığı çoğu durumda sınıf yazmadan da kurabiliriz:

from typing import Iterator

def adim_say(baslangic: int, son: int, adim: int) -> Iterator[int]:
  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)

Karşılaştırma:

  • durum yönetimini kendiniz ayrıntılı kontrol etmek istiyorsanız sınıf tabanlı iterator uygundur
  • sadece sırayla değer üretmek istiyorsanız generator çoğu zaman daha kısa ve nettir

Problem: Büyük Veri Setleri ve Bellek Sınırları

Ya işlememiz gereken veri çok büyükse? Milyonlarca satırlık bir dosya, sensörlerden gelen sürekli veri akışı, devasa bir veritabanı sorgusu sonucu…

# Milyarlarca sayının karesini hesaplamak istiyoruz diyelim
# numbers = list(range(1_000_000_000)) # Bu satır bilgisayarı kilitleyebilir! MemoryError!
# squares = []
# for n in numbers:
#   squares.append(n*n) # squares listesi de devasa olur!

Tüm veriyi aynı anda bir listeye veya başka bir yapıya yüklemek, hem çok fazla bellek (RAM) tüketir hem de çok uzun sürebilir. Bazen bu mümkün bile olmaz.

Çözüm: Veriyi parça parça, sadece ihtiyaç duyulduğu anda işlemek.

Üreteçler (Generators): “Tembel” ve Bellek Dostu Çözüm 🏭

Üreteç Nedir? Üreteçler, elemanları tek tek ve sadece istendiğinde üreten özel tür iteratorlardır. Normal bir fonksiyon gibi tanımlanırlar ama return yerine yield anahtar kelimesini kullanırlar.

Neden İhtiyaç Duyuldu? Büyük veri setleriyle çalışırken belleği verimli kullanmak için icat edildiler. Tüm sonuçları bir listede biriktirmek yerine, her sonucu ihtiyaç anında “üretip” verirler.

yield’ın Sihri: yield, fonksiyonun çalışmasını duraklatır, bir değer döndürür ve fonksiyonun o anki durumunu (lokal değişkenler, nerede kaldığı) hafızasında tutar. Fonksiyondan bir sonraki değer istendiğinde, kaldığı yerden çalışmaya devam eder.

yield İçin Zihinsel Model

Generator fonksiyonunu şöyle düşünmek işimizi kolaylaştırır:

  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. yield değeri dışarı verir ve fonksiyon orada durur.
  5. Bir sonraki next() çağrısında kaldığı yerden devam eder.

Kısaca akış şöyledir:

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

yield Nasıl Çalışır? Adım Adım İzleyelim

yield içeren bir fonksiyon (üreteç fonksiyonu) çağırdığımızda ne olur?

def basit_sayac_uretici(ust_limit):
  print(">>> Üreteç fonksiyonu çağrıldı, kod BAŞLIYOR...")
  n = 0
  while n < ust_limit:
    print(f">>> yield {n} öncesi")
    yield n  # DURAKLAMA NOKTASI! Değeri ver ve bekle.
    print(f">>> yield {n} sonrası (kaldığı yerden devam)")
    n += 1
  print(">>> Üreteç fonksiyonu bitti.")

generator_obj = basit_sayac_uretici(2)
print("--- Üreteç nesnesi oluşturuldu (kod henüz çalışmadı!) ---")
print(f"Nesne: {generator_obj}")

print("\n--- İlk next() ---")
sonuc1 = next(generator_obj) # Kod yield n'e kadar çalışır (n=0)
print(f"--- Dönen Değer: {sonuc1} ---") # 0 döner, fonksiyon yield'da duraklar.

print("\n--- İkinci next() ---")
sonuc2 = next(generator_obj) # Kod yield'dan sonra devam eder, döngü döner, yield n'e gelir (n=1)
print(f"--- Dönen Değer: {sonuc2} ---") # 1 döner, fonksiyon yield'da tekrar duraklar.

print("\n--- Üçüncü next() ---")
try:
  next(generator_obj) # Kod yield'dan sonra devam eder, döngü biter, fonksiyon sonlanır.
except StopIteration:
  print("--- StopIteration alındı (üreteç bitti) ---")

return ve yield Aynı Şey Değildir

return yield
Fonksiyonu bitirir Fonksiyonu duraklatır
Tek bir sonuç döndürür Birden fazla değeri sırayla üretebilir
Kaldığı yeri korumaz Kaldığı yeri korur

Bu yüzden generator fonksiyonu, “liste döndüren fonksiyon” ile aynı şey değildir.

Benzetme: yield’ı bir filmdeki duraklat düğmesi gibi düşünebilirsiniz. Kod bir noktada durur, değer dışarı verilir, sonra bir sonraki next() ile kaldığı yerden devam edilir.

Önemli Not: Üreteçler Tüketilebilir! ⚠️

  • Üreteçler (ve genel olarak iterator’lar) “tembel” oldukları ve değerleri tek tek ürettikleri için, elemanları üzerinde yalnızca bir kez gezilebilir.
  • Bir üreteç nesnesindeki tüm değerler for döngüsü, list(), sum() gibi bir yapıyla tüketildikten sonra boşalır.
  • Aynı üreteç nesnesi üzerinde ikinci kez döngü kurmaya çalışırsanız, hiçbir eleman vermez!
def sayi_uret(n):
  for i in range(n):
    yield i

generator_obj = sayi_uret(3)

print("İlk tüketim (liste oluşturma):")
liste1 = list(generator_obj) # Üreteç tüketilir
print(liste1) # Çıktı: [0, 1, 2]

print("\nİkinci tüketim denemesi:")
liste2 = list(generator_obj) # Üreteç zaten boş!
print(liste2) # Çıktı: []

# Eğer tekrar kullanmak isterseniz, üreteci yeniden oluşturmalısınız:
generator_yeni = sayi_uret(3)
print("\nYeni üreteçle tüketim:")
for sayi in generator_yeni:
  print(sayi) # Çıktı: 0, 1, 2

Bu, bellek verimliliğinin bir sonucudur. Üreteç, tüm değerleri saklamadığı için, tüketildikten sonra başa dönemez.

Iterator mı, Generator mı?

İkisi de aynı protokole uyar; fark daha çok yazım biçimi ve kullanım amacındadır.

  • bir nesnenin iç durumunu ayrıntılı yönetmeniz gerekiyorsa sınıf tabanlı iterator yazabilirsiniz
  • sadece sırayla değer üretmek istiyorsanız generator genelde daha kısa olur
  • dışarıdan okunabilirlik önemliyse, generator çoğu durumda daha az kodla aynı fikri anlatır

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

Üreteç İfadeleri: Tek Satırlık Üreteçler (...)

Liste comprehensions ([]) kullanarak anında liste oluşturabildiğimiz gibi, parantez (()) kullanarak da anında üreteç nesnesi oluşturabiliriz. Bu, basit dönüşümler için çok pratik bir kısa yoldur.

# Liste Comprehension (Bellekte tam liste oluşturur)
kareler_liste = [x * x for x in range(1000)]
# print(f"Liste boyutu (yaklaşık): {kareler_liste.__sizeof__()} bytes") # Bellek kullanımı artar

# Üreteç İfadesi (Sadece üreteç nesnesi oluşturur, değerler üretilmez)
kareler_uretec = (x * x for x in range(1000))
# print(f"Üreteç boyutu (yaklaşık): {kareler_uretec.__sizeof__()} bytes") # Çok daha küçük!

print(f"Üreteç nesnesi: {kareler_uretec}")

# Değerler sadece döngüde veya next() ile istendiğinde üretilir:
print("İlk 5 kare (üreteçten):")
for i in range(5):
  print(next(kareler_uretec)) # 0, 1, 4, 9, 16

# Veya tümünü tüketmek için:
# toplam = sum(kareler_uretec) # Kalan karelerin toplamını verimli hesaplar

Üreteç ifadeleri, “lazy evaluation” (tembel değerlendirme) prensibini kısa ve okunaklı bir şekilde uygular.

Tip İpuçları (Type Hints): Kodun Anlaşılırlık ve Güvenliği İçin 🏷️

Nedir? Kodun farklı bölümlerinin (değişkenler, fonksiyon parametreleri, dönüş değerleri) ne tür verilerle çalışması beklendiğini belirten standartlaşmış bir notasyon sistemidir.

Ne Değildir? Tip ipuçları, Python’ı Java veya C# gibi statik tipli bir dile dönüştürmez. Yorumlayıcı, çalışma zamanında tipleri zorunlu kılmaz (ek araçlar kullanmadıkça).

Neden Kullanılır?

  1. Okunabilirlik ve Anlaşılırlık: Fonksiyonun ne aldığını ve ne döndürdüğünü anlamak kolaylaşır.
  2. Hata Önleme: MyPy gibi statik analiz araçları, kod çalışmadan tip uyumsuzluklarını yakalar.
  3. Geliştirici Araçları (IDE) Desteği: Daha iyi otomatik tamamlama ve refactoring.
  4. Kod Bakımı ve İşbirliği: Büyük projelerde veya ekiplerde kodun niyetini açıklar.

Bu noktada önemli olan şudur:

Tip ipucu, çalışma zamanındaki davranışı tek başına değiştirmez; ama kodun sözleşmesini görünür hale getirir.

Sık Yanlış Anlama: Tip İpucu Zorunlu Kontrol Değildir

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

bir_yas_artir("20")

Bu imza şunu söyler:

  • bu 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 hatayı erken görmek için genelde IDE, Pylance veya mypy gibi araçlar kullanılır.

Tip İpuçları: Bu Haftanın Kavramlarına Bağlayalım

Bu hafta açısından tip ipuçları şu ayrımı görünür hale getirir:

  • 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

Bu yüzden tip ipuçları burada yalnızca genel bir notasyon değil, anlatılan akış modelinin imzadaki karşılığıdır.

Tip İpuçları: Pratik Kullanım

Temel tipler (int, str, float, bool, list, dict vb.) doğrudan kullanılabilir. Daha karmaşık durumlar için typing modülü devreye girer.

from typing import Any, Iterable, Iterator, Optional

# Temel tipler
ogrenci_no: int = 123
ders_adi: str = "NTP 2"

# Koleksiyon tipleri (içerik tipiyle belirtilir)
notlar: list[int] = [80, 90, 75]
ogrenci_bilgileri: dict[str, Any] = {"ad": "Ayşe", "no": 123, "aktif": True}

# None içerebilen tipler için Optional veya Union
aciklama: Optional[str] = None

# Birden fazla olası tip için | yazımı
kimlik: int | str = "ABC-123"

# Fonksiyon imzaları
def kayit_olustur(isim: str, yas: int) -> dict[str, Any]:
  """Yeni bir kayıt oluşturur ve sözlük olarak döndürür."""
  return {"ad": isim, "yas": yas}

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

# Iterator tipi
def harfleri_ver(kelime: str) -> Iterator[str]:
  """Kelimenin harflerini tek tek yield eder."""
  for harf in kelime:
    yield harf

Not:

  • yeni Python sürümlerinde list[int], dict[str, Any], int | str yazımı yaygındır
  • eski kaynaklarda List[int], Dict[str, Any], Union[int, str] biçimlerini de görebilirsiniz

Basit Örnek 1: Çift Sayıların Kareleri Üreteci

Bir listedeki çift sayıların karelerini yield eden, tip ipuçları kullanılmış bir üreteç fonksiyonu yazalım.

from typing import Iterator

def cift_kareleri(sayilar: list[int]) -> Iterator[int]:
  """Verilen listedeki çift sayıların karelerini yield eder."""
  for sayi in sayilar:
    if sayi % 2 == 0:
      yield sayi * sayi

# Kullanım
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 generator döndürüyor

Basit Örnek 2: Kelime Iterator Sınıfı

Bir cümlenin kelimeleri üzerinde gezinen, tip ipuçları kullanılmış özel bir iterator sınıfı oluşturalım.

from typing import Iterator

class KelimeIterator:
  def __init__(self, cumle: str) -> None:
    self.kelimeler: list[str] = cumle.split()
    self._index: int = 0

  def __iter__(self) -> Iterator[str]:
    return self

  def __next__(self) -> str:
    if self._index < len(self.kelimeler):
      kelime = self.kelimeler[self._index]
      self._index += 1
      return kelime
    raise StopIteration

# Kullanım
metin = "Bu basit bir cümledir"
kelime_gezici = KelimeIterator(metin)

print("Cümlenin kelimeleri:")
for k in kelime_gezici:
  print(k)

# Çıktı:
# Bu
# basit
# bir
# cümledir

Bu sınıf örneği özellikle şu ayrımı görünür kılar:

  • generator fonksiyonu daha kısa yol sunar
  • sınıf tabanlı iterator ise iç durumu daha açık gösterir

Özet: Güçlü Araçlarımızı Tekrar Hatırlayalım

  • iterable / iterator / generator ayrımı bu haftanın temel kavramsal çatısıdır.
  • for döngüsü, arka planda iter() ve next() çağrılarıyla çalışır.
  • Generator’lar, değerleri baştan depolamak yerine ihtiyaç anında üretir.
  • Generator’lar ve iterator’lar tek kullanımlık akış gibi düşünülmelidir.
  • Tip ipuçları, kodun sözleşmesini görünür kılar; denetimi ise ayrı araçlar güçlendirir.

Bu başlıkların ortak noktası, Python’da davranışı daha bilinçli okumayı sağlamalarıdır.

Sınıf İçi Alıştırmalar ✍️

Aşağıdaki problemleri Python’da çözmeye çalışın. İskeleti tamamlayın, sonra çıktıyı örnek veriyle test edin.

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: list(adim_say(1, 10, 2)) -> [1, 3, 5, 7, 9]
from typing import Iterator

def adim_say(baslangic: int, son: int, adim: int) -> Iterator[int]:
  # 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ı
  • örnek: list(SesliHarfIterator("Merhaba Dünya"))

İpucu:

  • sesli olmayan karakterlerde index değerini artırıp aramaya devam edin
  • metnin sonuna gelirseniz StopIteration üretin
from typing import Iterator

class SesliHarfIterator:
  def __init__(self, metin: str) -> None:
    self.metin = metin
    self.index = 0

  def __iter__(self) -> Iterator[str]:
    return self

  def __next__(self) -> str:
    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
kelimeler = ["Python", "programlama", "çok", "güzel", "bir", "dil"]

uzunluklar = (
  # burada generator expression yazın
)

for uzunluk in uzunluklar:
  print(uzunluk)

Kapanış Sorusu

Bir problemi çözerken şu üç soruyu sırayla sorun:

  1. Elimdeki şey bir koleksiyon mu, akış mı?
  2. Bu veriyi baştan depolamam gerekiyor mu?
  3. Bu fonksiyonun hangi tür veri beklediğini imzada açıkça göstermeli miyim?

Bu üç soruya verdiğiniz cevap, çoğu zaman sizi doğru araca götürür.

Teşekkürler