8 - Python’ın İleri Seviye Özellikleri II – Iteratorlar, Üreteçler ve Tip İpuçları
2025
for
döngüsünün iç yüzü.__iter__
, __next__
)yield
ile sihirli duraklatma. 🏭(...)
.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.
Peki, Python bu for item in my_list
ifadesini nasıl anlıyor ve arka planda neler oluyor? İşte burada Iterator Protokolü devreye giriyor.
__iter__
metoduna sahip olmalıdırlar.__iter__
(genellikle kendini döndürür) ve __next__
metodlarına sahiptir.for
Döngüsünün Çalışma Adımları:
for
döngüsü başlarken, iter(my_list)
çağrılır. Bu, listeden bir iterator (yer imi) nesnesi alır.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.next()
metodu özel bir sinyal olan StopIteration
hatasını fırlatır.for
döngüsü bu StopIteration
sinyalini yakalar ve döngüyü sonlandırır.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.
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.
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ç 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 İzleyelimyield
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.
for
döngüsü, list()
, sum()
gibi bir yapıyla tüketildikten sonra boşalır.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.
(...)
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.
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?
MyPy
gibi statik analiz araçları, kod çalışmadan tip uyumsuzluklarını yakalar.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.
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}")
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
__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).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!(...)
: Basit üreteçleri tek satırda, okunaklı bir şekilde oluşturmanın kısa yoludur. (Bunlar da tek kullanımlıktır).Bu araçları anlamak ve doğru yerde kullanmak, daha verimli, okunaklı ve sağlam Python programları yazmanızı sağlar.
Aşağıdaki problemleri Python’da çözmeye çalışın (Tip ipuçlarını kullanmayı unutmayın!):
adim_say(baslangic: int, son: int, adim: int) -> Iterator[int]
adında bir üreteç fonksiyonu yazın.baslangic
’tan başlayıp son
’a kadar (son dahil değil) adim
kadar artarak sayıları yield
etmeli.list(adim_say(1, 10, 2))
çıktısı [1, 3, 5, 7, 9]
olmalı.SesliHarfIterator
adında bir sınıf oluşturun.__init__
metodu bir string (metin: str
) alsın.__iter__
ve __next__
metodlarını uygun şekilde implemente edin.list(SesliHarfIterator("Merhaba Dünya"))
çıktısı ['e', 'a', 'a', 'ü', 'a']
(veya benzeri) olmalı.kelimeler = ["Python", "programlama", "çok", "güzel", "bir", "dil"]
listesi verilsin.for
döngüsü ile dolaşıp uzunlukları ekrana yazdırın.