Ders 08 / 16
Döngüler
Döngü bir denetim yapısı değil, bir protokoldür: üç öğelik nesnede dört kez __next__ çağrılır, sonuncusu StopIteration fırlatır ve döngüyü bitiren şey koşul değil, o istisnadır.
İçindekiler
Önceki ders doğruluk kuralını ölçtü: if arkasındaki nesne bir soruya sokuluyor ve o
sorunun yanıtı bir dalla bitiyordu. Sınama bir kez yapılıyor, sonuç bir kez okunuyordu.
Döngü aynı soruyu tekrarlamaz, başka bir soruyu tekrarlar: sırada bir öğe var mı?
Programlama Temelleri kursunun döngüler dersi bu yapıyı bir denetim yapısı olarak
kurdu — turların sayısı ya koleksiyonun uzunluğuna ya bir koşula bağlıydı ve döngünün
sonlanması, her turda koşulu yanlışlamaya yaklaşan bir değişiklikle güvenceye alınıyordu.
Python’da döngü bir protokoldür ve o dersin sonlanma gerekçesi burada geçerli değildir:
for döngüsünün sınadığı bir koşul yoktur. Peki turların bittiğine ne karar veriyor? Bu
dersin ölçtüğü şey budur — ve yanıtın bir hata sınıfı olduğu ortaya çıkacaktır.
Döngü Neyi Çağırır
for x in n yazımı üç adımlıdır ve üçü de gizlidir.
Önce nesneden bir yineleyici (iterator) istenir: __iter__ çağrılır.
Sonra her turda o yineleyiciden bir sonraki öğe
istenir: __next__ çağrılır. Üçüncü adım bitiştir — ve bitişi bildiren şey bir dönüş
değeri değildir.
Bir dönüş değeri olamazdı. __next__ herhangi bir nesne döndürebilir; hangi değeri
seçersek seçelim, o değer koleksiyonun gerçek bir öğesi olabilir ve “bitti” ile “işte
öğen” birbirine karışırdı. Bu yüzden bitiş kanaldan çıkarılmıştır: __next__, verecek
öğesi kalmadığında bir istisna fırlatır — StopIteration. Döngüyü bitiren şey budur.
Bunun doğrudan bir sonucu var: son bir çağrı, hiçbir öğe üretmeyen bir çağrı, her zaman
yapılır. Öğe sayısı ne olursa olsun __next__ bir fazla çağrılır ve o fazladan çağrı
döngünün bitiş bildirimidir.
Aynı Döngünün Elle Yazımı
Protokolün gerçekten bir kısaltma olduğunu göstermenin yolu, for kullanmadan aynı işi
yapmaktır: yineleyiciyi iter() ile al, bir sonsuz döngü kur, her turda next() çağır ve
StopIteration yakalandığında çık. İki yazım aynı protokol dizisini üretiyorsa for
gerçekten bu üç adımın kısaltmasıdır.
Ölçüme iki nesne daha girer. Izci hem __iter__ hem __getitem__ tanımlar; ikisi de
yinelemeye yeter, ama hangisinin kullanıldığını ancak sayım söyler. Dizinli yalnız
__getitem__ tanımlar ve __iter__ yoktur — böyle bir nesnenin döngüye girip
giremeyeceği, girerse hangi protokolü izleyeceği ikinci sorudur.
Ölçümün varsayımları:
- PA8 — Kâhin kurgunun kendisidir: her özel yöntem çağrıldığında adını kaydeder; “kaç kez çağrıldı” sorusunun ölçüsü bu kayıttır.
- PA9 —
Izciortak tanımdaki davranışını korur:__iter__sayacı sıfırlayıp kendisini döndürür,__next__öğeler bitinceStopIterationfırlatır. - PA10 — Öğe sayısı sıfırdan dörde kadar değiştirilir; sayının kendisi değil, çağrı sayısıyla arasındaki fark ölçülür.
- PA11 —
breakikinci öğede çalıştırılır; kesme noktasının yeri sabittir ve turların erken bitmesinin çağrı sayısına etkisini ayırmak için seçilmiştir. - PA12 — Ölçüm listeye alma yazımı kullanmaz, yalnız açık
forgövdesi kullanır; amacı, sayılan çağrıların yalnız yineleme protokolünden gelmesini güvenceye almaktır. - PA13 —
Dizinliyalnız__getitem__tanımlar;__iter__tanımsızlığı kasıtlıdır ve ikinci protokolün ölçülebilmesi için gereklidir. - PA14 —
whileölçümünde gövde her turda bir öğe eksiltir; sonlanma güvencesi buradan gelir ve sınama sayısıyla tur sayısı ayrı ayrı okunur. - PA15 — İç içe döngü ölçümü iki durumda yapılır: iki döngü aynı nesne üzerinde
ve ayrı nesneler üzerinde.
Izci.__iter__ortak tanımdaki gibi kendisini döndürür ve sayacı sıfırlar; ölçülen şey bu iki kararın tur sayısına etkisidir.
Ölçüm
"""Yineleme protokolu: dongu kac kez __next__ cagirir ve dongüyü ne bitirir.""" KAYIT = [] def kaydet(ad): KAYIT.append(ad) class Izci: """Hem __iter__ hem __getitem__ tanimli.""" def __init__(self, ogeler=(1, 2, 3)): self.ogeler = list(ogeler) def __iter__(self): kaydet("__iter__") self._i = 0 return self def __next__(self): kaydet("__next__") if self._i >= len(self.ogeler): raise StopIteration deger = self.ogeler[self._i] self._i += 1 return deger def __getitem__(self, k): kaydet("__getitem__") return self.ogeler[k] def __len__(self): kaydet("__len__") return len(self.ogeler) class Dizinli: """Yalniz __getitem__ tanimli: __iter__ yok.""" def __init__(self, ogeler=(1, 2, 3)): self.ogeler = list(ogeler) def __getitem__(self, k): kaydet("__getitem__") return self.ogeler[k] def olc(islev): KAYIT.clear() try: islev() except Exception as e: KAYIT.append(f"!{type(e).__name__}") return list(KAYIT) TUR = [] def tam(): for x in Izci(): TUR.append(x) def kesilen(): for x in Izci(): TUR.append(x) if x == 2: break def sonlu_else(): for x in Izci(): TUR.append(x) else: kaydet("else") def kesilen_else(): for x in Izci(): TUR.append(x) if x == 2: break else: kaydet("else") def elle(): y = iter(Izci()) while True: try: TUR.append(next(y)) except StopIteration: break def dorduncu(): y = iter(Izci()) for _ in range(4): next(y) BICIMLER = (("for x in n", tam), ("for + break", kesilen), ("for + else", sonlu_else), ("for + break + else", kesilen_else), ("iter/next elle", elle), ("dördüncü next", dorduncu)) print(f"{'biçim':<20s} {'tur':>4s} {'__next__':>9s} {'else':>5s} protokol dizisi") for ad, f in BICIMLER: TUR.clear() c = olc(f) p = [a for a in c if a != "else"] print(f" {ad:<18s} {len(TUR):4d} {c.count('__next__'):9d}" f" {('evet' if 'else' in c else '-'):>5s} {' '.join(p)}") print() print(f"{'öğe':>4s} {'tur':>4s} {'__next__':>9s} fark") for n in range(5): TUR.clear() def say(n=n): for x in Izci(range(n)): TUR.append(x) c = olc(say) print(f" {n:2d} {len(TUR):4d} {c.count('__next__'):9d}" f" {c.count('__next__') - n:+d}") print() print(f"{'nesne':<10s} {'tanımlı':<24s} {'çağrı':>6s} protokol dizisi") for ad, kur, tanimli in ( ("Izci", Izci, "__iter__ ve __getitem__"), ("Dizinli", Dizinli, "yalnız __getitem__")): def gez(k=kur): for _ in k(): pass c = olc(gez) print(f" {ad:<8s} {tanimli:<24s} {len(c):6d} {' '.join(c)}") print(" dördüncü dizin doğrudan:", " ".join(olc(lambda: Dizinli()[3]))) DIS = [] def ic_ice_ayni(): n = Izci() for x in n: DIS.append(x) for y in n: TUR.append(y) def ic_ice_ayri(): for x in Izci(): DIS.append(x) for y in Izci(): TUR.append(y) print() print(f"{'iç içe döngü':<16s} {'dış tur':>8s} {'iç tur':>7s}" f" {'__iter__':>9s} {'__next__':>9s}") for ad, f in (("aynı nesne", ic_ice_ayni), ("ayrı nesneler", ic_ice_ayri)): TUR.clear() DIS.clear() c = olc(f) print(f" {ad:<14s} {len(DIS):8d} {len(TUR):7d}" f" {c.count('__iter__'):9d} {c.count('__next__'):9d}") def dongu_while(): n = Izci() while n: n.ogeler.pop() print() c = olc(dongu_while) print(f"while n (3 öğe boşalana dek): çağrı {len(c)} {' '.join(c)}")
biçim tur __next__ else protokol dizisi for x in n 3 4 - __iter__ __next__ __next__ __next__ __next__ for + break 2 2 - __iter__ __next__ __next__ for + else 3 4 evet __iter__ __next__ __next__ __next__ __next__ for + break + else 2 2 - __iter__ __next__ __next__ iter/next elle 3 4 - __iter__ __next__ __next__ __next__ __next__ dördüncü next 0 4 - __iter__ __next__ __next__ __next__ __next__ !StopIteration öğe tur __next__ fark 0 0 1 +1 1 1 2 +1 2 2 3 +1 3 3 4 +1 4 4 5 +1 nesne tanımlı çağrı protokol dizisi Izci __iter__ ve __getitem__ 5 __iter__ __next__ __next__ __next__ __next__ Dizinli yalnız __getitem__ 4 __getitem__ __getitem__ __getitem__ __getitem__ dördüncü dizin doğrudan: __getitem__ !IndexError iç içe döngü dış tur iç tur __iter__ __next__ aynı nesne 1 3 2 6 ayrı nesneler 3 9 4 16 while n (3 öğe boşalana dek): çağrı 4 __len__ __len__ __len__ __len__
Bir Fazla Çağrı
Üst tablonun ilk satırı ortak tanımın üçüncü iddiasıdır. Üç öğelik bir nesnede döngü
üç tur dönüyor ama __next__ dört kez çağrılıyor. Fazladan olan dördüncü çağrı
hiçbir öğe üretmiyor; üretmediğini bildirmek için var.
İkinci tablo bunun tek bir örneğe bağlı olmadığını gösteriyor. Öğe sayısı sıfırdan dörde çıkarken fark sütunu değişmiyor: her satırda +1. Sıfır öğeli nesnede bile bir çağrı var — hiç tur dönmeyen döngü bile bir kez sormak zorunda, çünkü sormadan boş olduğunu bilemez. Çağrı sayısı öğe sayısının bir fonksiyonu değil, öğe sayısı artı bitiş bildirimidir.
Son satır o bildirimi doğrudan gösteriyor. Yineleyici elle alınıp next() dört kez
çağrıldığında dördüncüsü bir değer döndürmüyor, StopIteration fırlatıyor. for
döngüsünde bu istisnayı görmeyiz çünkü döngü onu kendisi yakalar ve akışı sessizce
gövdenin dışına taşır. Görünmemesi olmadığı anlamına gelmez: döngünün her normal
bitişinde bir istisna doğar ve yakalanır.
Buradan çıkan okuma ortak tanımın üçüncü iddiasıdır: istisna bir hata değil, protokolün parçasıdır. Bir hata olsaydı sıra dışı bir durumu bildirirdi; oysa bu istisna en sıradan durumu bildiriyor — koleksiyon bitti.
Kısaltmanın Açılımı
Beşinci satır for yazımının gerçekten bir kısaltma olduğunu kanıtlıyor. Elle yazılan
biçim — iter(), sonsuz döngü, next(), StopIteration yakalama — birebir aynı
protokol dizisini üretiyor: bir __iter__, dört __next__. İki yazım arasında
yürütme farkı yok; fark yalnız try bloğunun görünür olmasında.
İkinci ve dördüncü satırlar break ile kesilen döngüyü ölçüyor ve buradaki sayı diğer
hepsinden ayrılıyor: iki tur, iki çağrı. Fazladan çağrı yok. break döngüyü
protokolün dışından bitiriyor; üçüncü öğe hiç istenmiyor, StopIteration hiç doğmuyor.
Kesilen döngü, tek fazladan çağrının ortadan kalktığı tek biçimdir.
Aynı konunun öbür denetim deyimi continue bu tabloda ayrı bir satır gerektirmez, çünkü
protokole hiç dokunmaz: gövdenin kalanını atlar ve döngüyü bir sonraki __next__
çağrısına taşır. Turların sayısı değişmez, fazladan çağrı yerinde durur. break ile
arasındaki fark tam da budur — biri protokolün içinde bir adım atlar, öbürü protokolü
terk eder.
Bu, else sütununu da açıklıyor. Döngüye bağlı else bloğu, döngü kendi protokolüyle
bittiğinde çalışır; break ile kesildiğinde çalışmaz. Üçüncü satırda else çalışıyor,
dördüncüde çalışmıyor. Yani else, “hiç break olmadan bitti mi?” sorusunun yanıtıdır —
bir başka deyişle, StopIteration gerçekten fırlatıldı mı? Aranan bir öğeyi bulunca
break eden ve bulunamadığında else bloğunda davranan arama kalıbı doğrudan bu ayrımın
üzerine kuruludur.
İkinci Protokol
Üçüncü tablo hangi protokolün seçildiğini ölçüyor ve önceki dersin örüntüsünü tekrarlıyor.
Izci hem __iter__ hem __getitem__ tanımlar. Döngü __iter__’i seçiyor ve
__getitem__ hiç çağrılmıyor. Doğruluk kuralında __bool__ __len__’i nasıl devre dışı
bıraktıysa burada da __iter__ __getitem__’i devre dışı bırakıyor; iki yöntem
yarışmıyor, ilki ikincisini kapatıyor.
Dizinli satırı ikinci basamağı gösteriyor. Bu nesnede __iter__ yok, ama döngü yine de
çalışıyor: __getitem__ sıfırdan başlayarak artan dizinlerle dört kez çağrılıyor.
Yineleyicisi olmayan bir nesne için dil ikinci bir yineleme yolu tanır — sırayla dizin
iste, bir dizin geçersiz olana kadar.
İki rolü ayırmak gerekir. Yinelenebilir bir nesne __iter__ tanımlayan nesnedir;
kendisi gezilmez, gezecek olanı verir. Yineleyici ise __next__ tanımlayan nesnedir
ve asıl gezme işini o yapar. Bir nesnenin ikisini birden tanımlaması yasak değildir —
Izci tam olarak bunu yapar ve __iter__ gövdesinde kendisini döndürür. Bu yüzden
Izci hem yinelenebilir hem yineleyicidir; ayrım kaybolduğunda ortaya çıkan sonuç aşağıda
ölçülüyor.
Sayı yine dört, öğe sayısından bir fazla. Ama fazladan çağrıyı bitiren istisna bu kez
başka: son satır Dizinli()[3] çağrısını doğrudan yapıyor ve çıkan şey StopIteration
değil, IndexError. İki protokol, iki ayrı bitiş istisnası, aynı +1 düzeni. Her
iki yolda da döngünün bittiğini söyleyen şey bir dönüş değeri değil, fırlatılan bir
istisnadır.
Çarpım Kuralının Bozulduğu Yer
Dördüncü tablo iç içe iki döngüyü ölçüyor ve Programlama Temelleri kursunun iç içe döngüler için verdiği kuralı — toplam tur sayısı ikisinin çarpımıdır — sınıyor. Ayrı nesneler üzerinde kural tutuyor: dış 3 tur, iç 9 tur, yani üç kere üç.
Aynı nesne üzerinde tutmuyor. Dış döngü 3 değil 1 tur dönüyor; iç döngü 9
değil 3. Çarpım kuralı çöküyor ve bunun nedeni döngünün kendisi değil, __iter__’in
ne döndürdüğü.
Izci.__iter__ yeni bir nesne üretmez; kendisini döndürür ve sayacı sıfırlar. Dış
döngü ilk öğesini aldıktan sonra iç döngü aynı nesne üzerinde __iter__ çağırıyor, sayaç
sıfırlanıyor, iç döngü üç öğeyi de tüketip sayacı sona getiriyor. Dış döngü sıradaki öğeyi
istediğinde sayaç zaten sonda: StopIteration geliyor ve dış döngü birinci turunda
bitiyor. İki döngü aynı sayacı paylaşıyor.
Buradaki ayrım, yinelenebilir bir nesne ile yineleyicinin ayrı şeyler olmasıdır. Her
__iter__ çağrısında taze bir yineleyici döndüren bir nesne iç içe döngülerde
beklendiği gibi davranır; kendisini döndüren bir nesne, aynı anda birden çok kez
gezilemez. Sözdizim ikisinde de aynıdır, tur sayısı değildir — ve hangi davranışın
geçerli olduğunu söyleyen tek şey __iter__’in gövdesidir. Nesnenin tipine, adına ya da
kaç öğe taşıdığına bakarak bu ayrımı görmek olanaksızdır.
Son satır while için de aynı düzeni veriyor. Üç öğe boşalana kadar dönen döngü
__len__’i dört kez çağırıyor: üç tur için üç sınama, bir de koşulu yanlışlayan
dördüncü sınama. for bir __next__ fazla çağırır, while bir doğruluk sınaması fazla
yapar. Fazladan olan, her iki durumda da döngüyü bitiren sınamadır.
Özet
- Python’da döngü bir denetim yapısı değil, bir protokoldür:
for x in nönce__iter__çağırır, sonra her tur için__next__; sınadığı bir koşul yoktur. - Üç öğelik nesnede
__next__dört kez çağrılır ve fark öğe sayısından bağımsızdır — sıfır öğede bile bir çağrı yapılır, çünkü boşluk ancak sorularak öğrenilir. - Fazladan çağrı bir değer döndürmez,
StopIterationfırlatır; döngüyü bitiren şey budur veforbu istisnayı kendisi yakaladığı için görünmez. İstisna bir hata değil, protokolün parçasıdır. foryazımıiter,nextveStopIterationyakalamanın kısaltmasıdır; elle yazılan açılım birebir aynı çağrı dizisini üretir.breakise döngüyü protokolün dışından bitirir ve fazladan çağrıyı ortadan kaldırır —elsebloğu tam olarak bu ayrımı okur.__iter__tanımlıyken__getitem__yinelemeye hiç katılmaz; yalnız__getitem__tanımlı bir nesnede döngü artan dizinlerle onu çağırır ve bu yolu bitiren istisnaIndexError’dır.- İç içe döngülerin çarpım kuralı,
__iter__kendisini döndürdüğünde bozulur: aynı nesne üzerinde iki döngü tek sayacı paylaşır ve dış döngü 3 yerine 1 tur döner; ayrı nesnelerde kural yine tutar (3 ve 9).
Sonraki Adım
Döngü gövdesi çoğu zaman bir işlev çağırır ve o işleve her turda başka değerler geçer. Programlama Temelleri kursu parametre ile argüman ayrımını ve çağrı çerçevesini kurmuştu; Python’da argüman geçirmenin biçimleri o dersin varsaydığından fazladır — konumsal ve anahtarlı argümanlar, değişken sayıda argüman toplama, varsayılan değerler. Bunların içinde bir tanesi zamanlamayla ilgilidir ve sonraki dersin ölçtüğü şey odur: bir parametrenin varsayılan değeri ne zaman hesaplanır — her çağrıda mı, yoksa bir kez mi? Yanıt, değiştirilebilir bir varsayılanla birlikte şaşırtıcı bir sayı üretir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.