İçeriğe geç
academia.sh

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: Oge her üretimini sayar ve paylaşım is ile gösterilir; “kaç nesne var edildi” sorusunun ölçüsü bu sayaçtır.
  • YN2 — Ortak tanımın Oge sı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 is bakımından aynı olduğudur.
  • YN6Sayim öğ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 next ikinci 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ığında StopIteration fırlatılır.
  • Geçmek, kopyalamak ve yeniden üretmek üç ayrı iştir: iter yeni 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat