8 - Python’ın İleri Seviye Özellikleri II – Iteratorlar, Üreteçler ve Tip İpuçları
2026
Ö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?Bu üç soru birlikte düşünülünce konu sadece sözdizimi değil, aynı zamanda doğru model kurma meselesidir.
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ıyield kullanan üreteç arasında ne fark olduğunuGeçen haftalardaki iki fikri burada birleştiriyoruz:
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:
iterableiteratorgeneratorArdından bu akışı tip ipuçlarıyla daha açık hale getireceğiz.
for Gerçekte Ne Yapıyor?Bu kısa örnek şunu gösterir:
next(...) ile tek tek istenirYani for döngüsünü anlamak için önce bu küçük mekanizmayı anlamamız gerekir.
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 isterfor döngüsünün perde arkasına açılan kapıdırİşte burada Iterator Protokolü devreye giriyor.
| 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:
Örneğin bir listeyi doğrudan next() ile tüketemezsiniz; önce iter(...) ile iterator almanız gerekir.
Bir nesnenin for içinde çalışabilmesi için Python şu beklentiye sahiptir:
iter(nesne) çağrılabilmelinext(...) uygulanabilmeliTeknik olarak burada olan şey şudur:
next(iterator_nesnesi) çağrılır__next__() metodunu tetiklerfor 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(iterator_nesnesi) çağrılır. Bu çağrı sıradaki elemanı döndürür ve iterator’ı bir sonraki elemana ilerletir.next(iterator_nesnesi) çağrısı özel bir sinyal olan StopIteration hatasını üretir.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: 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:
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:
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 İçin Zihinsel ModelGenerator fonksiyonunu şöyle düşünmek işimizi kolaylaştırır:
next() geldiğinde kod ilk yield noktasına kadar ilerler.yield değeri dışarı verir ve fonksiyon orada durur.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 İ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) ---")return ve yield Aynı Şey Değildirreturn |
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.
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, 2Bu, bellek verimliliğinin bir sonucudur. Üreteç, tüm değerleri saklamadığı için, tüketildikten sonra başa dönemez.
İkisi de aynı protokole uyar; fark daha çok yazım biçimi ve kullanım amacındadır.
Pratikte çoğu akış üretimi işinde önce generator düşünmek iyi bir başlangıçtır.
(...)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.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.
Bu imza şunu söyler:
int bekliyorint üretmeyi amaçlıyorAma 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.
Bu hafta açısından tip ipuçları şu ayrımı görünür hale getirir:
Iterable[int]: üzerinde dolaşılabilen kaynakIterator[int]: sıradaki int değerini veren nesneIterator[T] döndürürBu yüzden tip ipuçları burada yalnızca genel bir notasyon değil, anlatılan akış modelinin imzadaki karşılığıdır.
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 harfNot:
list[int], dict[str, Any], int | str yazımı yaygındırList[int], Dict[str, Any], Union[int, str] biçimlerini de görebilirsinizBir 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]?
int üreten bir generator döndürüyorBir 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ümledirBu sınıf örneği özellikle şu ayrımı görünür kılar:
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.Bu başlıkların ortak noktası, Python’da davranışı daha bilinçli okumayı sağlamalarıdır.
Aşağıdaki problemleri Python’da çözmeye çalışın. İskeleti tamamlayın, sonra çıktıyı örnek veriyle test edin.
İstenen davranış:
adim_say(baslangic, son, adim) fonksiyonu yield kullanmalıbaslangictan başlayıp son değerine gelmeden bitmelilist(adim_say(1, 10, 2)) -> [1, 3, 5, 7, 9]İstenen davranış:
list(SesliHarfIterator("Merhaba Dünya"))İpucu:
index değerini artırıp aramaya devam edinStopIteration üretinİstenen davranış:
Bir problemi çözerken şu üç soruyu sırayla sorun:
Bu üç soruya verdiğiniz cevap, çoğu zaman sizi doğru araca götürür.