İçeriğe geç
academia.sh

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.
  • PA9Izci ortak tanımdaki davranışını korur: __iter__ sayacı sıfırlayıp kendisini döndürür, __next__ öğeler bitince StopIteration fı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.
  • PA11break ikinci öğ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 for gövdesi kullanır; amacı, sayılan çağrıların yalnız yineleme protokolünden gelmesini güvenceye almaktır.
  • PA13Dizinli yalnız __getitem__ tanımlar; __iter__ tanımsızlığı kasıtlıdır ve ikinci protokolün ölçülebilmesi için gereklidir.
  • PA14while ö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, StopIteration fırlatır; döngüyü bitiren şey budur ve for bu istisnayı kendisi yakaladığı için görünmez. İstisna bir hata değil, protokolün parçasıdır.
  • for yazımı iter, next ve StopIteration yakalamanın kısaltmasıdır; elle yazılan açılım birebir aynı çağrı dizisini üretir. break ise döngüyü protokolün dışından bitirir ve fazladan çağrıyı ortadan kaldırır — else bloğ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 istisna IndexError’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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat