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
Ogesınıfı veN = 1000sayı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 ikisindenextile. - 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
closeile bırakılır;finallybloğ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
yieldiç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,yieldaskı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 gdoğ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ığındareturndeğeriStopIterationüzerinde taşınır veforile gezen taraf onu görmez. yield fromara kap kurmadan yetki devreder: ilk öğeye dek 1 üretim. Aynı işi iki listeyi ekleyerek yapmak ilk öğeden önce 1000 üretim ve üç kap gerektirir.yieldbir ifadedir vesendile gönderilen değeri döndürür; bin öğelik akış üç öğeden sonracloseile bırakıldığında üretim 3’te kalır ama gövdedekifinallybloğ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.