Ders 07 / 16
Yineleyici Protokolü
Öğeleri görmek için kabı yeniden kurmak gerekmez: dilim kopyası üç başvuruyu yeni bir listeye yazarken yineleyici sıfır yazar, üçünü de paylaşır ve hiçbir öğe var etmez.
İçindekiler
Önceki ders kopyanın bir katman derinliğinde kaldığını ölçtü: dilimle alınan liste yeni bir kaptı, ama içindeki her öğe eskisiyle aynı nesneydi. Yine de o kabı kurmak bedava değildi. Yeni liste öğeleri çoğaltmıyordu, fakat her birinin başvurusunu kendi içine yazmak zorundaydı — kopya öğeleri üretmiyor, hepsine dokunuyordu.
Bu dersin sorusu bir adım öteye gider. Bir koleksiyonun öğelerini baştan sona görmek için o kabı yeniden kurmak gerekli mi? Öğelere hiç dokunmadan, hiçbir yeni kap kurmadan üzerlerinden geçmek mümkün mü? Python’un yanıtı yineleyici (iterator) sözleşmesidir ve bu ders o sözleşmeyi kursun ekseninden ölçer: geçiş kaç nesne var ediyor, kaçını tutuyor, kaçını paylaşıyor?
Sözleşme
Sözleşme iki yerleşik ve iki özel yöntemden ibarettir. iter(n) bir nesneden yineleyici
ister ve nesnenin __iter__ yöntemini çağırır. next(y) yineleyiciden sıradaki öğeyi
ister ve __next__ çağırır. Verecek öğe kalmadığında __next__ bir değer döndürmez,
StopIteration fırlatır.
Python Temelleri kursunun döngüler dersi bu protokolü kullanan taraftan ölçmüştü: for
yazımının hangi özel yöntemi kaç kez çağırdığı ve döngüyü bitiren şeyin bir dönüş değeri
değil bir istisna olduğu orada kuruldu; o ölçüm burada tekrarlanmaz. Bu dersin tarafı
öbürüdür — sözleşmeyi yazan taraf. Aynı protokole uyan iki nesne dışarıdan tamamen aynı
görünür, ama biri öğeleri önceden var edip tutar, öbürü istendikçe var edip hiç tutmaz.
Sözdizim bu farkı söylemez; sayı söyler.
Yinelenebilir ile Yineleyici
Sözleşme iki rol tanımlar ve karıştırılmaları en sık görülen kusurdur. Yinelenebilir
bir nesne __iter__ tanımlar; kendisi gezilmez, gezecek olanı verir. Yineleyici
__next__ tanımlar ve asıl gezme işini o yapar. Liste bir yinelenebilirdir ama yineleyici
değildir: next(liste) çalışmaz, önce iter(liste) çağrılması gerekir.
Bir nesnenin ikisini birden tanımlaması yasak değildir. O zaman __iter__ gövdesinde
kendisini döndürür ve ayrım kaybolur; sonucu aşağıda ölçülüyor. Ayrımın ayakta kaldığı
durumda __iter__ her çağrıldığında taze bir yineleyici üretir.
Buradan çıkan tanım kursun eksenine oturur: yineleyicinin taşıdığı şey öğeler değil, nerede kalındığıdır. Bir konum bir öğe değildir ve öğe sayısıyla büyümez.
Ölçümün varsayımları:
- YN1 — Kâhin kurgunun kendisidir:
Ogeher üretimini sayar ve paylaşımisile gösterilir; “kaç nesne var edildi” sorusunun ölçüsü bu sayaçtır. - YN2 — Ortak tanımın
Ogesınıfı sayma düzeniyle birlikte aynen kullanılır. - YN3 — Kaynak üç öğelidir. Sayı küçük tutulmuştur, çünkü ölçülen şey büyüklük değil, geçişin kap kurup kurmadığıdır.
- YN4 — “Kapta tutulan”, geçişten çıkan nesnenin kendi içine yazdığı öğe başvurusu sayısıdır: bir liste için uzunluğu, bir yineleyici için sıfır. Ölçü sayılan başvurudur, ölçülen bayt değil.
- YN5 — “Paylaşılan”, geziden çıkan öğelerden kaçının kaynaktaki nesneyle
isbakımından aynı olduğudur. - YN6 —
Sayimöğeleri istendikçe var eder ve hiçbirini saklamaz;Kapöğeleri kuruluşta var edip bir listede tutar. İki sınıf yalnız bu noktada ayrılır ve ikisi de aynı sözleşmeyi imzalar. - YN7 — Tek geçişlilik ölçümü aynı nesne üzerinde
list()iki kez çağrılarak yapılır; ikinci çağrının döndürdüğü öğe sayısı ölçüdür. - YN8 — Nöbetçi ölçümünde
nextikinci argümanla çağrılır ve tükenmiş yineleyici üzerinde iki kez denenir; amacı tükenmenin kalıcı olduğunu göstermektir. - YN9 — İki geçiş ölçümünde üç yolun üçü de aynı beş öğelik kaynağı ister; değişen tek şey öğelerin nereden geldiğidir. Var edilen sayısı iki geçişin toplamıdır, tutulan ise geçişler arasında canlı kalan başvuru sayısıdır.
Ölçüm
"""Yineleyici sozlesmesi: gecis kac nesne var eder, kac basvuru tutar.""" SAYAC = {"uretilen": 0} class Oge: """Her uretimi sayan oge.""" def __init__(self, deger): SAYAC["uretilen"] += 1 self.deger = deger def __repr__(self): return f"Oge({self.deger})" def sifirla(): SAYAC["uretilen"] = 0 def uretilen(): return SAYAC["uretilen"] def tutulan(nesne): """Nesnenin canli tuttugu Oge sayisi: kendisinde ya da bir liste ozniteliginde.""" if isinstance(nesne, list): return sum(isinstance(o, Oge) for o in nesne) sayi = 0 for d in getattr(nesne, "__dict__", {}).values(): if isinstance(d, list): sayi += sum(isinstance(o, Oge) for o in d) return sayi KAYNAK = [Oge(i) for i in range(3)] BICIMLER = (("iter(kaynak)", lambda: iter(KAYNAK)), ("kaynak[:]", lambda: KAYNAK[:]), ("list(kaynak)", lambda: list(KAYNAK)), ("reversed(kaynak)", lambda: reversed(KAYNAK)), ("kavrama ile kopya", lambda: [Oge(o.deger) for o in KAYNAK])) print(f"{'geçiş biçimi':<20s} {'yeni kap':>9s} {'var edilen':>11s}" f" {'kapta tutulan':>14s} {'paylaşılan':>11s}") for ad, kur in BICIMLER: sifirla() nesne = kur() kap = "liste" if isinstance(nesne, list) else "yok" tut = tutulan(nesne) ortak = sum(1 for o in list(nesne) if any(o is k for k in KAYNAK)) print(f" {ad:<18s} {kap:>9s} {uretilen():11d} {tut:14d} {ortak:11d}") y = iter(KAYNAK) print() print(f"iter(kaynak) is kaynak -> {y is KAYNAK}, iter(y) is y -> {iter(y) is y}") print(f"next(y) is kaynak[0] -> {next(y) is KAYNAK[0]}") a, b = iter(KAYNAK), iter(KAYNAK) print(f"ayrı iki yineleyici: {len(list(a))} ve {len(list(b))} öğe") t = iter(KAYNAK) print(f"aynı yineleyici iki kez: {len(list(t))} ve {len(list(t))} öğe") class Sayim: """Ogeleri istendikce var eden yineleyici.""" def __init__(self, n): self.n, self.i = n, 0 def __iter__(self): return self def __next__(self): if self.i >= self.n: raise StopIteration self.i += 1 return Oge(self.i - 1) class Kap: """Ogeleri onceden var edip tutan yinelenebilir.""" def __init__(self, n): self.ogeler = [Oge(i) for i in range(n)] def __iter__(self): return iter(self.ogeler) print() print(f"{'nesne':<10s} {'kurulumda':>9s} {'kurulumda tutulan':>18s}" f" {'iki öğe sonra':>14s} {'__iter__ verdiği':>17s}") for ad, kur in (("Sayim(5)", lambda: Sayim(5)), ("Kap(5)", lambda: Kap(5))): sifirla() n = kur() kurulum, kurulum_tut = uretilen(), tutulan(n) g = iter(n) next(g), next(g) print(f" {ad:<8s} {kurulum:9d} {kurulum_tut:18d} {uretilen():14d}" f" {('kendisi' if iter(n) is n else 'başka nesne'):>17s}") s = Sayim(3) print() print(f"Sayim(3) birinci geçiş {len(list(s))} öğe, ikinci geçiş {len(list(s))} öğe") print(f"tükendikten sonra: {next(s, 'nöbetçi')!r} ve {next(s, 'nöbetçi')!r}") k = Kap(3) print(f"Kap(3) birinci geçiş {len(list(k))} öğe, ikinci geçiş {len(list(k))} öğe") kalanlar = [3, 2, 1, 0, 9] print(f"iter(çağrılabilir, nöbetçi) -> {list(iter(lambda: kalanlar.pop(0), 0))}") def dogrudan(): s = Sayim(5) return len(list(s)), len(list(s)), 0 def listeye_al(): s = list(Sayim(5)) return len(list(s)), len(list(s)), len(s) def yeniden_kur(): return len(list(Sayim(5))), len(list(Sayim(5))), 0 print() print(f"{'iki geçiş yolu':<28s} {'var edilen':>11s} {'tutulan':>8s}" f" {'birinci':>8s} {'ikinci':>7s}") for ad, f in (("aynı yineleyiciyi gezmek", dogrudan), ("listeye alıp gezmek", listeye_al), ("her geçişte yeniden kurmak", yeniden_kur)): sifirla() bir, iki, tut = f() print(f" {ad:<26s} {uretilen():11d} {tut:8d} {bir:8d} {iki:7d}")
geçiş biçimi yeni kap var edilen kapta tutulan paylaşılan iter(kaynak) yok 0 0 3 kaynak[:] liste 0 3 3 list(kaynak) liste 0 3 3 reversed(kaynak) yok 0 0 3 kavrama ile kopya liste 3 3 0 iter(kaynak) is kaynak -> False, iter(y) is y -> True next(y) is kaynak[0] -> True ayrı iki yineleyici: 3 ve 3 öğe aynı yineleyici iki kez: 3 ve 0 öğe nesne kurulumda kurulumda tutulan iki öğe sonra __iter__ verdiği Sayim(5) 0 0 2 kendisi Kap(5) 5 5 5 başka nesne Sayim(3) birinci geçiş 3 öğe, ikinci geçiş 0 öğe tükendikten sonra: 'nöbetçi' ve 'nöbetçi' Kap(3) birinci geçiş 3 öğe, ikinci geçiş 3 öğe iter(çağrılabilir, nöbetçi) -> [3, 2, 1] iki geçiş yolu var edilen tutulan birinci ikinci aynı yineleyiciyi gezmek 5 0 5 0 listeye alıp gezmek 5 5 5 5 her geçişte yeniden kurmak 10 0 5 5
Kap Kurmayan Geçiş
Üst tablo beş geçiş biçimini yan yana koyuyor ve onları üç sınıfa ayırıyor.
Kabı yeniden kuranlar. kaynak[:] ve list(kaynak) yeni bir liste üretiyor ve o
listeye 3 başvuru yazıyor. Var edilen Oge sayısı 0 — hiçbir öğe çoğaltılmıyor,
paylaşılan 3. Bu, önceki dersin ölçtüğü yüzeysel kopyadır: dış kap yeni, içindekiler
eski.
Öğeleri yeniden üretenler. Son satırdaki kavrama 3 yeni Oge var ediyor ve
paylaşılan 0’a düşüyor. Kap da yeni, içindekiler de yeni.
Hiçbirini yapmayanlar. iter(kaynak) ve reversed(kaynak) satırlarında yeni kap
yok, kapta tutulan 0, var edilen 0, paylaşılan yine 3. Geçiş üç öğenin
üçünü de teslim ediyor, hiçbirini üretmiyor ve hiçbirinin başvurusunu kendi içine yazmıyor.
Girişteki soru buradan yanıtlanıyor: öğelere dokunmadan üzerlerinden geçmek mümkün.
Bir itiraz burada haklıdır: iter(kaynak) de bir nesne üretir — yineleyicinin kendisi.
Ama o nesne bir kap değildir; içinde öğe tutmaz, yalnız kaynağa bir başvuru ve bir
konum taşır. Tablodaki “var edilen” sütunu Oge sayar, “kapta tutulan” sütunu ise öğe
başvurusu; yineleyici ikisinde de sıfır veriyor çünkü ikisinden de değil.
Fark üç öğede bir başvuru meselesi gibi görünür. Kaynak bin öğeli olsaydı list(kaynak)
bin başvuru yazacak, iter(kaynak) yine sıfır yazacaktı. Kabın maliyeti öğe sayısıyla
büyür, yineleyicininki büyümez: tutulan sayı öğe sayısının bir fonksiyonudur, konum
değildir.
Yineleyici Ne Taşır
Orta bloktaki üç satır rolleri ayırıyor. iter(kaynak) is kaynak yanlış: liste bir
yinelenebilirdir, ama gezme işini kendisi yapmaz; iter ondan ayrı bir nesne döndürür.
iter(y) is y ise doğru: bir yineleyiciden yineleyici istendiğinde kendisi gelir. Bu,
for döngüsünün hem koleksiyonları hem yineleyicileri aynı yazımla kabul edebilmesinin
nedenidir — ikisine de iter uygulanır ve ikincisinde bu çağrı bir şey değiştirmez.
next(y) is kaynak[0] doğru: teslim edilen öğe kaynağın öğesinin kendisidir, kopyası
değil. Yineleyici bir üretici değil, bir teslimatçıdır.
Sonraki iki satır konumun nerede tutulduğunu gösteriyor. Aynı listeden alınan ayrı iki yineleyici birbirinden bağımsız: her biri 3 öğe veriyor. Aynı yineleyici iki kez gezildiğinde 3 ve 0. Konum listede değil yineleyicide durduğu için, listeden istenen her yeni yineleyici sıfırdan başlar; var olan bir yineleyici ise kaldığı yerden devam eder ve sonunda tükenir.
Sözleşmeyi Yazan İki Sınıf
İkinci tablo aynı sözleşmeyi imzalayan iki sınıfı karşılaştırıyor. Sayim kurulumda 0
nesne var ediyor ve 0 tutuyor; iki öğe istendiğinde üretilen 2’ye çıkıyor. Kap
kurulumda 5 var ediyor, 5 tutuyor ve iki öğe istendikten sonra sayı yine 5 —
üretim çoktan bitmişti.
İki nesne dışarıdan ayırt edilemez. İkisi de for döngüsüne girer, ikisi de aynı beş
değeri verir, ikisinde de iter ve next çalışır. Ayrım yalnız sayıdadır: biri işi
kuruluşta, öbürü istendikçe yapar.
Son sütun ikinci ayrımı veriyor. Sayim.__iter__ kendisini döndürüyor; Kap.__iter__
her çağrıda iç listesinden başka bir nesne alıyor. Bunun sonucu alttaki iki satırda:
Sayim(3) birinci geçişte 3, ikinci geçişte 0 öğe veriyor — tükeniyor. Kap(3)
her iki geçişte de 3 veriyor, çünkü her geçiş taze bir yineleyiciyle başlıyor.
Tek geçişlilik bir kusur değil, sözleşmenin doğrudan sonucudur. Sözleşme “istendiğinde sıradaki öğeyi ver” der; “aynı öğeleri ikinci kez de ver” demez. Bir yineleyicinin ikinci geçişi verebilmesi için gördüğü her öğeyi saklaması gerekirdi — yani tam olarak kaçındığı şeyi yapması. Tutmayan nesne geri saramaz.
Tükenme kalıcıdır. Boşalmış Sayim üzerinde next iki kez daha çağrıldığında ikisinde de
nöbetçi değer geliyor; nesne kendini yenilemiyor.
İki Geçişin Bedeli
Son tablo bu sonucun pratikteki karşılığını üç yolla ölçüyor. Elde tek geçişlik bir kaynak var ve öğeler iki kez lazım.
Aynı yineleyiciyi iki kez gezmek işe yaramıyor: 5 ve 0. Var edilen 5, tutulan 0 — en ucuz satır, ama ikinci geçişi vermiyor.
Listeye alıp iki kez gezmek ikinci geçişi veriyor: 5 ve 5. Var edilen yine 5, çünkü öğeler bir kez üretilip saklandı. Bedel tutulan sütununda: 0’dan 5’e çıktı. Öğe sayısı bin olsaydı bu sütun bin olacaktı.
Her geçişte kaynağı yeniden kurmak da ikinci geçişi veriyor ve tutulan 0’da kalıyor. Bedeli öbür sütunda: var edilen 5’ten 10’a çıktı, çünkü beş öğe iki kez üretildi. Bu yol ancak kaynak yeniden kurulabiliyorsa geçerlidir; bir ağ akışı ya da bir kullanıcı girdisi ikinci kez aynı öğeleri vermeyebilir.
Üç satırın hiçbiri ötekilerin toplamını yenmiyor ve ortada bir seçim var: tutmadan iki geçiş olmaz. Ya öğeleri saklarsınız, ya yeniden üretirsiniz, ya da tek geçişe razı olursunuz. Yineleyici bu seçimi ortadan kaldırmaz; yalnız üçüncü seçeneği ücretsiz kılar.
Sözleşmenin İki Kaçış Kapısı
Son iki satır sözleşmenin yerleşik düzeyindeki iki esnekliğini gösteriyor.
next(y, nöbetçi) ikinci argümanla çağrıldığında StopIteration fırlatmaz, verilen değeri
döndürür. İstisna bir değere çevrilmiş olur; try bloğu kurmadan “bitti mi?” sorusunu
sormanın yoludur. Nöbetçi olarak seçilen değerin akışta geçemeyecek bir değer olması
gerekir, yoksa bitiş ile öğe birbirine karışır — bu, StopIteration’ın neden bir dönüş
değeri olmadığının aynı gerekçesidir.
iter iki argümanla çağrıldığında bambaşka bir şey yapar: kaynağı bir koleksiyon değil,
bir çağrı olan yineleyici üretir. Örnekte her next bir listeden baştan bir öğe alıyor
ve nöbetçi olan 0 göründüğünde geçiş bitiyor. Sonuç [3, 2, 1] — nöbetçinin kendisi
teslim edilmiyor. Böylece dosya, kuyruk ya da her çağrıda bir sonrakini veren herhangi bir
kaynak, koleksiyona hiç dönüştürülmeden for döngüsüne sokulabilir.
Özet
- Yineleyici sözleşmesi iki yerleşik ve iki özel yöntemdir:
iter__iter__’i,next__next__’i çağırır; öğe kalmadığındaStopIterationfırlatılır. - Geçmek, kopyalamak ve yeniden üretmek üç ayrı iştir:
iteryeni kap kurmaz ve 0 başvuru yazar,list(kaynak)3 yazar, kavrama ayrıca 3 yeni nesne var eder. Üçünde de paylaşım ölçülür ve yalnız sonuncusunda 0’dır. - Yinelenebilir
__iter__tanımlar, yineleyici__next__tanımlar;iter(liste)listeden ayrı bir nesne verir,iter(yineleyici)nesnenin kendisini verir. Yineleyicinin taşıdığı şey öğeler değil, kalınan konumdur. - Aynı sözleşmeyi imzalayan iki sınıf dışarıdan ayırt edilemez ama sayıları ayrıdır: kurulumda 0’a karşı 5 üretim, 0’a karşı 5 tutma.
- Tek geçişlilik sözleşmenin sonucudur:
__iter__kendisini döndüren nesne 3 ve 0 öğe verir, taze yineleyici döndüren nesne iki geçişte de 3. Tutmayan nesne geri saramaz ve tükenme kalıcıdır.
Sonraki Adım
Sayim sınıfı beş satırda üç yöntem, bir sınır ve bir konum alanı istedi; hepsi tek bir
şey için — nerede kalındığını hatırlamak. Bu, elle yazılan her yineleyicide tekrarlanan bir
iştir ve konum tek bir sayı olmadığında (iç içe bir gezinti, dallanmış bir arama) sınıf
hızla büyür. Sonraki ders, dilin bu işi yazana bırakmayan biçimini ölçüyor: gövdesi
duraklayıp kaldığı yerden devam eden bir işlev. Ölçülecek sayı şudur — böyle bir işlevi
çağırmak, gövdesindeki bin üretimden kaçını çağrı anında yapar?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.