İçeriğe geç
academia.sh

Ders 08 / 16

Üreteçler

Üreteç işlevini çağırmak gövdesinden tek satır çalıştırmaz: bin öğelik iş kavramada kurulumda 1000 nesne var ederken üreteçte 0 var eder ve üç öğe istendiğinde 3'te kalır.

İçindekiler

Önceki ders yineleyici sözleşmesini ölçtü ve onu elle yazan bir sınıf kurdu: Sayim beş satırda iki özel yöntem, bir sınır ve bir konum alanı istedi. Hepsi tek bir iş içindi — nerede kalındığını hatırlamak. O iş her yineleyicide tekrarlanır ve konum tek bir sayı olmadığında sınıf hızla büyür. İç içe bir gezinti için birden çok sayaç, dallanmış bir arama için bir yığıt gerekir; her biri elle taşınmalıdır.

Oysa sıradan bir işlev bu bilgiyi zaten taşır. Bir işlev çalışırken hangi satırda olduğu, yerel adlarının hangi değerlere bağlı olduğu bellidir; yalnız işlev bittiğinde hepsi kaybolur. Bu dersin sorusu şudur: işlev bitmek yerine duraklayabilseydi, konumu ayrıca yazmaya gerek kalır mıydı? Python’un yanıtı üreteç (generator) işlevidir ve ölçülecek sayı şudur — böyle bir işlevi çağırmak, gövdesindeki bin üretimden kaçını çağrı anında yapar?

Duraklayan İşlev

Bir işlev gövdesinde return yerine yield geçiyorsa o işlev artık sıradan bir işlev değildir. Çağrıldığında gövdesi çalışmaz; geriye bir üreteç nesnesi döner. Gövde ancak o nesneden bir öğe istendiğinde çalışmaya başlar ve ilk yield deyimine gelince durur. Durduğu yerde yerel adlar, döngü sayaçları ve çağrı çerçevesi olduğu gibi kalır. Bir sonraki istek geldiğinde yürütme yield’in hemen ardından devam eder.

Ayrım burada nettir: return çerçeveyi yıkar, yield askıya alır. Programlama Temelleri kursu tembel değerlendirmeyi bir paradigma tercihi olarak tanıtmıştı — değerin kullanıldığı ana kadar hesaplanmaması. Burada ölçülen şey o tercihin adı değil, Python’un onu hangi düzenekle verdiği ve kaç nesneye mal olduğudur.

Üreteç Bir Yineleyicidir

Üreteç nesnesi önceki dersin sözleşmesini imzalar: __iter__ ve __next__ tanımlıdır, __iter__ kendisini döndürür, gövde bittiğinde StopIteration fırlatılır. Yani üreteç, Sayim sınıfının elle yazdığı şeyin dil tarafından yazılmış hâlidir — sözleşmenin yazan tarafı, ama sınıf kurmadan.

Buradan iki sonuç doğrudan gelir. Üreteç tek geçişliktir, çünkü __iter__ kendisini döndürür. Ve tükendikten sonra yenilenmez, çünkü gövdesi sona ulaşmıştır. İkisi de önceki derste sözleşmenin sonucu olarak kurulmuştu; burada bir kez daha ölçülür.

Ölçümün varsayımları:

  • YN10 — Ortak tanımın Oge sınıfı ve N = 1000 sayısı aynen kullanılır; sayma düzeni değiştirilmez.
  • YN11 — Üç biçim de aynı işi tanımlar: bin öğelik bir akış. Değişen tek şey yazımdır; kâhin kurgunun kendisidir, çünkü üretimi nesne kendi sayar.
  • YN12 — “Kurulumda” ölçümü, biçimin yazıldığı satır çalıştıktan hemen sonra alınır. “Üç öğe sonra” ölçümü, üç öğe istendikten sonra alınır; kavramada bu istek liste[:3] ile yapılır, öbür ikisinde next ile.
  • YN13 — İz kaydı gövdenin içine yerleştirilmiş bir listedir; her satır hangi noktaya ulaşıldığını yazar. “Gövde çalıştı mı” sorusunun ölçüsü bu listedir.
  • YN14 — İz ölçümünde öğe sayısı üçtür; ölçülen şey sayı değil, çağrı ile gövdenin ilerlemesi arasındaki ilişkidir.
  • YN15 — Sonsuz üreteç ölçümü kasıtlıdır: kurulumun sıfır üretmesi, öğe sayısının sonlu olmasından gelmiyor.
  • YN16 — Birleştirme ölçümünde iki kanat 400 ve 600 öğelidir; toplam yine 1000’dir ve üç biçim aynı öğe dizisini verir.
  • YN17 — “İlk öğeye dek” sütunu, birleşimden yalnız bir öğe alındıktan sonraki üretim sayısıdır; biçimler arasındaki farkı bu sütun ayırır.
  • YN18 — Kapanış ölçümünde akış bin öğeliktir ama üç öğe alındıktan sonra close ile bırakılır; finally bloğuna konan iz satırı o anki üretim sayısını da yazar, böylece kapanışın ne zaman çalıştığı ölçülebilir.

Ölçüm

"""Uretec: govde ne zaman calisir, kurulum kac nesne var eder."""

SAYAC = {"uretilen": 0}
IZ = []


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"]


N = 1000


def uretec_islevi(n=N):
    for i in range(n):
        yield Oge(i)


def kavrama(n=N):
    sifirla()
    liste = [Oge(i) for i in range(n)]
    kurulumda = uretilen()
    ilk_uc = liste[:3]
    return kurulumda, uretilen(), len(ilk_uc)


def uretec_ifadesi(n=N):
    sifirla()
    akis = (Oge(i) for i in range(n))
    kurulumda = uretilen()
    ilk_uc = [next(akis) for _ in range(3)]
    return kurulumda, uretilen(), len(ilk_uc)


def uretec_cagrisi(n=N):
    sifirla()
    akis = uretec_islevi(n)
    kurulumda = uretilen()
    ilk_uc = [next(akis) for _ in range(3)]
    return kurulumda, uretilen(), len(ilk_uc)


print(f"{'biçim':<24s} {'kurulumda':>10s} {'üç öğe sonra':>13s} {'elde':>5s}")
for ad, f in (("kavrama", kavrama), ("üreteç ifadesi", uretec_ifadesi),
              ("üreteç işlevi çağrısı", uretec_cagrisi)):
    a, b, c = f()
    print(f"  {ad:<22s} {a:10d} {b:13d} {c:5d}")


def izli(n=3):
    IZ.append("gövde başladı")
    for i in range(n):
        IZ.append(f"yield {i} öncesi")
        yield Oge(i)
        IZ.append(f"yield {i} sonrası")
    IZ.append("gövde bitti")
    return "son"


IZ.clear()
g = izli()
print()
print(f"çağrıdan sonra iz: {IZ}")
next(g)
print(f"birinci next sonrası: {IZ}")
next(g)
print(f"ikinci next sonrası: {IZ}")
print(f"üreteç yineleyici mi: iter(g) is g -> {iter(g) is g}")

IZ.clear()
son = izli(1)
next(son)
try:
    next(son)
except StopIteration as e:
    print(f"gövde bittiğinde: {IZ[-1]!r}, istisnanın taşıdığı değer {e.value!r}")

tek = uretec_islevi(3)
print()
print(f"üreteç işlevi tek geçişlik: {len(list(tek))} ve {len(list(tek))} öğe")


def sonsuz():
    i = 0
    while True:
        yield Oge(i)
        i += 1


sifirla()
akis = sonsuz()
kurulum = uretilen()
[next(akis) for _ in range(3)]
print(f"sonsuz üreteç: kurulumda {kurulum}, üç öğe istendiğinde {uretilen()}")


def kanat(a, b):
    yield from uretec_islevi(a)
    yield from uretec_islevi(b)


def elle_kanat(a, b):
    for o in uretec_islevi(a):
        yield o
    for o in uretec_islevi(b):
        yield o


def ara_liste(a, b):
    return list(uretec_islevi(a)) + list(uretec_islevi(b))


print()
print(f"{'birleştirme biçimi':<22s} {'kurulumda':>10s} {'ilk öğeye dek':>14s}"
      f" {'tümü gezilince':>15s}")
for ad, f in (("yield from", kanat), ("iç içe for + yield", elle_kanat),
              ("iki listeyi ekleme", ara_liste)):
    sifirla()
    n = f(400, 600)
    kurulumda = uretilen()
    y = iter(n)
    next(y)
    ilk = uretilen()
    list(y)
    print(f"  {ad:<20s} {kurulumda:10d} {ilk:14d} {uretilen():15d}")


def toplayici():
    toplam = 0
    try:
        while True:
            gelen = yield toplam
            toplam += gelen
    finally:
        IZ.append("toplayıcı kapandı")


def kaynak(n=N):
    try:
        for i in range(n):
            yield Oge(i)
    finally:
        IZ.append(f"kaynak kapandı, o ana dek üretilen {uretilen()}")


IZ.clear()
t = toplayici()
ilk = next(t)
print()
print(f"yield bir ifadedir: ilk değer {ilk}, send(5) -> {t.send(5)},"
      f" send(7) -> {t.send(7)}")
t.close()
print(f"close çağrısından sonra iz: {IZ}")

IZ.clear()
sifirla()
akis = kaynak()
uc = [next(akis) for _ in range(3)]
akis.close()
print(f"bin öğelik akıştan üç öğe alınıp bırakıldı: elde {len(uc)},"
      f" üretilen {uretilen()}")
print(f"kapanış izi: {IZ}")
biçim                     kurulumda  üç öğe sonra  elde
  kavrama                      1000          1000     3
  üreteç ifadesi                  0             3     3
  üreteç işlevi çağrısı           0             3     3

çağrıdan sonra iz: []
birinci next sonrası: ['gövde başladı', 'yield 0 öncesi']
ikinci next sonrası: ['gövde başladı', 'yield 0 öncesi', 'yield 0 sonrası', 'yield 1 öncesi']
üreteç yineleyici mi: iter(g) is g -> True
gövde bittiğinde: 'gövde bitti', istisnanın taşıdığı değer 'son'

üreteç işlevi tek geçişlik: 3 ve 0 öğe
sonsuz üreteç: kurulumda 0, üç öğe istendiğinde 3

birleştirme biçimi      kurulumda  ilk öğeye dek  tümü gezilince
  yield from                    0              1            1000
  iç içe for + yield            0              1            1000
  iki listeyi ekleme         1000           1000            1000

yield bir ifadedir: ilk değer 0, send(5) -> 5, send(7) -> 12
close çağrısından sonra iz: ['toplayıcı kapandı']
bin öğelik akıştan üç öğe alınıp bırakıldı: elde 3, üretilen 3
kapanış izi: ['kaynak kapandı, o ana dek üretilen 3']

Kurulumun Bedeli

Üst tablo ortak tanımın ikinci iddiasının ilk yarısıdır ve üç satır aynı işi tanımlar: bin öğelik bir akış.

Kavrama daha yazıldığı satırda 1000 nesne var ediyor. Üç öğe istendiğinde sayı değişmiyor — çünkü zaten hepsi üretilmişti; liste[:3] yalnız var olanlardan üçünü gösteriyor.

Üreteç ifadesi ve üreteç işlevi çağrısı kurulumda 0 var ediyor. Üç öğe istendiğinde 3’e çıkıyor. Aradaki 997 nesne hiç doğmuyor, çünkü kimse istemedi.

İki sayı arasındaki uçurum yazımda görünmez. Üç satırın üçü de tek bir deyimdir, üçü de bin öğelik bir akışı adlandırır, üçü de sonrasında for döngüsüne sokulabilir. Kurulumun 1000 mi 0 mı olduğunu söyleyen tek şey köşeli ayraç ile parantez arasındaki fark ve yield anahtar sözcüğüdür.

Buradaki tembelliğin sınırını doğru okumak gerekir. 0, işin yapılmadığı anlamına gelmez; henüz yapılmadığı anlamına gelir. Üç öğe istendiğinde üç nesne doğuyor; bin öğe istenirse bin nesne doğacak. Bu dersin ölçtüğü sayı yalnız ne zaman sorusunun yanıtıdır; toplamda kaç sorusunun yanıtı bir sonraki dersin konusudur.

üreteç ifadesi satırının yazımı — köşeli ayraç yerine parantez — bu dersin konusu değildir; sonraki ders onu kendi başına ölçer. Burada yalnız kurulum sayısını vermek için tabloda duruyor.

Gövde Nerede Duruyor

Orta blok gövdenin ilerleyişini iz kaydıyla gösteriyor ve ilk satır en şaşırtıcı olanıdır: üreteç işlevi çağrıldıktan sonra iz boş. Gövdeden tek satır çalışmamış. Çağrı bir işi başlatmamış, bir nesne kurmuş.

Birinci next çağrısından sonra iz iki satır: gövde başladı ve ilk yield’e kadar geldi. Orada durdu. İkinci çağrıdan sonra iki satır daha eklendi — yürütme yield’in ardından devam etti, döngünün bir sonraki turuna geçti ve ikinci yield’de yine durdu. Yerel i adı iki çağrı arasında korundu; kimse onu bir yere yazmadı, çerçeve olduğu gibi askıda bekledi.

Bu, Sayim sınıfının elle yaptığı işin tam karşılığıdır. Orada konum bir öznitelikti ve her çağrıda elle ilerletiliyordu; burada konum gövdenin kendisidir ve dil onu koruyor. iter(g) is g satırı sözleşmenin öbür ucunu doğruluyor: üreteç kendi yineleyicisidir. Tek geçişlilik de aynı yerden geliyor — 3 ve 0.

Bitişin Taşıdığı Değer

Gövde sona ulaştığında iz son satırını yazıyor ve StopIteration doğuyor. İlginç olan istisnanın boş olmaması: return "son" deyiminin değeri istisnanın üzerinde taşınıyor.

Bu, return ile yield’in bir üreteçte nasıl bölüştüğünü gösterir. yield akışın öğelerini verir; return akışın sonucunu verir ve akışın parçası değildir. Bir üreteci for ile gezen taraf bu değeri hiç görmez, çünkü döngü StopIteration’ı yakalayıp atar. Değeri isteyen tarafın istisnayı kendisi yakalaması gerekir.

Sonsuz üreteç satırı tembelliğin sonluluğa bağlı olmadığını gösteriyor: gövdesi hiç bitmeyen bir döngü olan üreteç kurulumda yine 0 var ediyor ve üç öğe istendiğinde 3’te kalıyor. Böyle bir akışı kavramayla yazmak olanaksızdır — kavrama bitmeyen bir kaynağı tüketmeye çalışır ve hiçbir zaman ilk satırdan çıkmaz. Tembellik burada bir eniyileme değil, ifade edilebilirlik koşuludur.

Yetki Devri

Son tablo iki akışı birleştirmenin üç yolunu ölçüyor ve üçü de aynı 1000 öğeyi veriyor.

yield from bir üreteci başka bir üretecin öğelerini teslim etmekle görevlendirir: dış üreteç kendi gövdesini askıya alır ve istekleri iç üretece devreder. Kurulumda 0, ilk öğe alındığında 1. for döngüsüyle elle yazılan biçim aynı sayıları veriyor; yield from bu döngünün kısaltmasıdır ve iç üretecin bitiş değerini de aktarır.

İki listeyi ekleme satırı ayrılıyor. Kurulumda 1000 — çünkü list() çağrıları iki kanadı da sonuna kadar tüketiyor. İlk öğeye ulaşmadan önce bin nesne doğmuş ve iki ara liste kurulmuş oluyor; üstelik + işleci üçüncü bir liste daha üretir. Sonuç aynı, sıra aynı, öğeler aynı; ayrılan tek şey ilk öğenin kaç üretimden sonra geldiğidir.

Ölçüm bir zincirin nerede kırıldığını da söyler: tembel bir akışa tek bir list() çağrısı girdiğinde zincirin o noktasında tembellik biter. Sonraki halkalar tembel yazılmış olsa bile kendilerinden önceki bin üretimi geri alamaz.

Kanalın İki Yönü ve Kapanış

Son dört satır yield’in tek yönlü bir çıkış olmadığını gösteriyor. yield bir deyim değil bir ifadedir ve bir değere sahiptir: askıdaki üreteç send ile uyandırıldığında, duran yield ifadesi gönderilen değeri döndürür ve gövde oradan devam eder. Ölçümdeki toplayıcı send(5) ile 5, send(7) ile 12 veriyor; toplam iki çağrı arasında gövdenin yerel adında duruyor.

İlk next çağrısı bu düzende zorunludur: gövde henüz ilk yield’e ulaşmamışsa gönderilen değeri alacak bir ifade yoktur. Bu yüzden send ile beslenen her üreteç önce bir kez ilerletilir; ölçümdeki 0 o ilk yield’in verdiği başlangıç değeridir.

Son iki satır tembel akışların en kolay atlanan yanını ölçüyor. Bin öğelik akıştan üç öğe alınıp akış bırakıldığında üretilen 3’te kalıyor — buraya kadar beklenen. Beklenmedik olan finally bloğunun çalışmasıdır: close çağrısı askıdaki gövdeyi durduğu noktadan uyandırır, orada bir istisna doğurur ve finally bloklarını yürütür. İz satırı bunu kanıtlıyor, üstelik o anki üretim sayısını da yazıyor.

Bunun pratikteki karşılığı şudur: bir üreteç açık bir kaynak üzerinde çalışıyorsa, akışın yarıda bırakılması o kaynağı asılı bırakmaz. Python Temelleri kursunun bağlam yöneticileri dersi serbest bırakmanın istisna durumunda da yapıldığını kurmuştu; üreteç gövdesindeki try/finally aynı güvenceyi askıya alınmış bir çerçeve için verir. Tembel olan işi erteler, sorumluluğu ertelemez.

Özet

  • yield içeren bir işlev çağrıldığında gövdesinden tek satır çalışmaz; geriye bir üreteç nesnesi döner. return çerçeveyi yıkar, yield askıya alır.
  • Bin öğelik akışta kavrama kurulumda 1000 nesne var eder, üreteç ifadesi ve üreteç işlevi çağrısı 0; üç öğe istendiğinde ikisi 3’te kalır. Yazımlar tek deyimlik ve benzerdir, sayılar değildir.
  • Üreteç önceki dersin sözleşmesini imzalar: iter(g) is g doğrudur, akış tek geçişliktir ve ikinci geçiş 0 öğe verir. Elle yazılan yineleyici sınıfının yaptığı işi dil yapar.
  • Gövde her yield’de durur ve bir sonraki istekle o noktadan devam eder; yerel adlar arada korunur. Sona ulaşıldığında return değeri StopIteration üzerinde taşınır ve for ile gezen taraf onu görmez.
  • yield from ara kap kurmadan yetki devreder: ilk öğeye dek 1 üretim. Aynı işi iki listeyi ekleyerek yapmak ilk öğeden önce 1000 üretim ve üç kap gerektirir.
  • yield bir ifadedir ve send ile gönderilen değeri döndürür; bin öğelik akış üç öğeden sonra close ile bırakıldığında üretim 3’te kalır ama gövdedeki finally bloğu yine çalışır.

Sonraki Adım

Bu ders yalnız kurulum anını ölçtü ve orada uçurum vardı: 1000’e karşı 0. Ama akış er ya da geç tüketilir — bin öğe gerçekten toplanacaksa üreteç de o bin nesneyi üretmek zorunda. Sonraki ders tam bu soruyu sorar: aynı işi sonuna kadar götüren iki yazım, işin sonunda kaç nesne üretmiş olur ve kaçını hâlâ elinde tutar? Yanıtın ilk yarısı beklenen, ikinci yarısı beklenmedik çıkacak — ve tembelliğin gerçekte neyi azalttığı ancak orada görünecek.

İ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