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

2025

Bu hafta: Python’un Güçlü Araçları

  • İterasyon Mekanizması: for döngüsünün iç yüzü.
  • Iterator Protokolü: Veriye adım adım nasıl erişiriz? (__iter__, __next__)
  • Üreteçler (Generators): Neden varlar? yield ile sihirli duraklatma. 🏭
  • Üreteç İfadeleri: Kısa yoldan üreteçler (...).
  • Tip İpuçları (Type Hints): Kodumuza netlik ve güvenlik katmak. 🏷️
  • Basit Uygulama Örnekleri
  • Alıştırmalar

İterasyon: Koleksiyonlarda Gezinmek

for döngüsü Python’da en sık kullandığımız yapılardan biri. Bir listedeki, string’deki veya başka bir koleksiyondaki elemanları kolayca gezmemizi sağlar.

my_list = [10, 20, 30]
for item in my_list:
  print(item) # 10, 20, 30

Peki, Python bu for item in my_list ifadesini nasıl anlıyor ve arka planda neler oluyor? İşte burada Iterator Protokolü devreye giriyor.

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

  • Iterable (Yinelenebilir): İçindeki elemanlar üzerinden geçilebilen, bir bütün olarak düşünülebilen nesnelerdir. Bir kitap gibi düşünün. (Örn: Liste, string, dosya, sözlük, set). __iter__ metoduna sahip olmalıdırlar.
  • Iterator (Yineleyici): Bir iterable’dan elemanları sırayla ve tek tek getiren nesnedir. Nerede kaldığını hatırlar. Kitaptaki bir yer imi gibi düşünebilirsiniz. __iter__ (genellikle kendini döndürür) ve __next__ metodlarına sahiptir.

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, bu iterator nesnesi üzerinden next() metodu çağrılır. next(), sıradaki elemanı (kitabın o anki sayfasını) döndürür ve yer imini bir sonraki elemana ilerletir.
  3. Tüm elemanlar bitince (kitabın sonuna gelince), next() metodu özel bir sinyal olan StopIteration hatasını fırlatır.
  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, son, adim):
    self.mevcut = baslangic
    self.son = son
    self.adim = adim

  def __iter__(self): # Iterator protokolü: __iter__ kendini döndürmeli
    # Neden self? Çünkü bu nesnenin kendisi zaten bir iterator.
    # iter(adimli_sayici_nesnesi) çağrıldığında, tekrar kendisini vermeli.
    return self

  def __next__(self): # Iterator protokolü: __next__ sıradaki değeri vermeli
    if self.mevcut >= self.son:
      raise StopIteration # Sona ulaşıldı, sinyali ver
    deger = self.mevcut
    self.mevcut += self.adim # Bir sonraki adıma geç
    return deger

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

__iter__ ve __next__ metodlarını doğru şekilde implemente etmek yeterlidir. Bir nesne iterator ise, __iter__ genellikle self döndürür.

Üreteçler (Generators): Bellek Dostu Çözüm

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 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) ---")

Benzetme: yield’ı bir filmdeki “pause” (duraklat) düğmesi gibi düşünün. Filmi (fonksiyonu) başlatırsınız, yield’a gelince durur ve o anki sahneyi (değeri) size verir. Tekrar “play” (next()) dediğinizde kaldığı yerden devam eder.

Ö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.

Ü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.

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 List, Dict, Tuple, Optional, Union, Any, Iterable, Iterator

# 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 # Union[str, None] ile aynı

# Birden fazla olası tip için Union
kimlik: Union[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."""
  # ... kod ...
  return {"ad": isim, "yas": yas}

# Iterable (üzerinden geçilebilir) ve Iterator (tek tek veren) tipleri
def harfleri_ver(kelime: str) -> Iterator[str]:
  """Kelimenin harflerini tek tek yield eder."""
  for harf in kelime:
    yield harf

typing modülü, Python’un tip sistemini ifade etmek için zengin araçlar sunar.

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 List, Iterator

def cift_kareleri(sayilar: List[int]) -> Iterator[int]:
  """Verilen listedeki çift sayıların karelerini yield eder."""
  print("Çift kareleri üreteci çalışıyor...")
  for sayi in sayilar:
    if sayi % 2 == 0:
      kare = sayi * sayi
      print(f"  -> {sayi} çift, karesi {kare} yield ediliyor.")
      yield kare
    else:
      print(f"  -> {sayi} tek, atlanıyor.")

# Kullanım
rakamlar = [1, 2, 3, 4, 5, 6]
kare_uretici = cift_kareleri(rakamlar)

print("İlk çift karenin alınması:")
print(f"Sonuç: {next(kare_uretici)}") # 2'nin karesi 4

print("\nKalan kareler için döngü:")
for k in kare_uretici: # Kaldığı yerden devam eder (4 ve 6 için)
  print(f"Sonuç: {k}")

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 List, Iterator

class KelimeIterator:
  def __init__(self, cumle: str) -> None:
    self.kelimeler: List[str] = cumle.split() # Cümleyi kelimelere ayır
    self._index: int = 0 # Mevcut kelimenin indeksi

  def __iter__(self) -> Iterator[str]: # Kendisi iterator, kendini döndür
    return self

  def __next__(self) -> str: # Sıradaki kelimeyi döndür
    if self._index < len(self.kelimeler):
      kelime = self.kelimeler[self._index]
      self._index += 1
      return kelime
    else:
      raise StopIteration # Kelimeler bitti

# 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

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

  • Iterator Protokolü (__iter__, __next__): Python’da koleksiyonlar üzerinde tutarlı gezinmenin temelidir. for döngüsünün arkasındaki sihirbazdır. (Iterator’lar __iter__ ile genelde self döndürür).
  • Üreteçler (Generators - yield): Bellek verimliliği şampiyonları! Değerleri ihtiyaç anında üretirler, büyük veri setleri veya sonsuz diziler için idealdirler. Fonksiyonun durumunu koruyarak duraklatıp devam etme yeteneği sunarlar. Dikkat: Sadece bir kez tüketilebilirler!
  • Üreteç İfadeleri (...): Basit üreteçleri tek satırda, okunaklı bir şekilde oluşturmanın kısa yoludur. (Bunlar da tek kullanımlıktır).
  • Tip İpuçları (Type Hints): Kodumuzun niyetini açıkça belirtmemizi sağlar. Okunabilirliği artırır, statik analiz araçlarıyla hataları erken yakalamamıza yardımcı olur ve modern Python geliştirmenin önemli bir parçasıdır.

Bu araçları anlamak ve doğru yerde kullanmak, daha verimli, okunaklı ve sağlam Python programları yazmanızı sağlar.

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

Aşağıdaki problemleri Python’da çözmeye çalışın (Tip ipuçlarını kullanmayı unutmayın!):

  1. Adım Sayacı Üreteci:
    • adim_say(baslangic: int, son: int, adim: int) -> Iterator[int] adında bir üreteç fonksiyonu yazın.
    • Bu fonksiyon, baslangic’tan başlayıp son’a kadar (son dahil değil) adim kadar artarak sayıları yield etmeli.
    • Örnek: list(adim_say(1, 10, 2)) çıktısı [1, 3, 5, 7, 9] olmalı.
  1. Sesli Harf Iterator’ı:
    • SesliHarfIterator adında bir sınıf oluşturun.
    • __init__ metodu bir string (metin: str) alsın.
    • Bu sınıf, verilen metindeki sadece sesli harfleri (a, e, ı, i, o, ö, u, ü - büyük/küçük fark etmez) sırayla döndüren bir iterator olsun.
    • __iter__ ve __next__ metodlarını uygun şekilde implemente edin.
    • Örnek: list(SesliHarfIterator("Merhaba Dünya")) çıktısı ['e', 'a', 'a', 'ü', 'a'] (veya benzeri) olmalı.
  1. Kelime Uzunlukları (Üreteç İfadesi):
    • kelimeler = ["Python", "programlama", "çok", "güzel", "bir", "dil"] listesi verilsin.
    • Bu listedeki 5 karakterden uzun kelimelerin uzunluklarını içeren bir üreteç ifadesi yazın.
    • Bu üreteci bir for döngüsü ile dolaşıp uzunlukları ekrana yazdırın.

Teşekkürler