- Yineleme protokolünü (
iter,next,StopIteration) açıklamak vefordöngüsünün içinde ne olduğunu anlatmak __iter__ve__next__ile bir yineleyici sınıfı,yieldile bir üreteç fonksiyonu yazmak- Tembel değerlendirmeyle sonsuz diziler ve işlem hatları kurmak
yield fromvesend()özelliklerini kullanmak
for döngüsü listelerle, karakter dizileriyle, sözlüklerle, dosyalarla, range ve zip ile çalışır; belleğe asla sığmayacak 10 GB'lık bir günlük (log) dosyasıyla bile. Tek bir döngü bu kadar farklı nesneyi nasıl dolaşabiliyor? Cevap yineleme protokolüdür (iteration protocol): for ile dolaştığı nesneler arasındaki küçük bir anlaşma. Bunu anladığında değerleri yalnızca gerektiğinde üreten nesneler yazabilecek, sonsuz veri akışlarını işleyebilecek ve belleği az kullanan işlem hatları kurabileceksin. Bunun temel aracı, Python'un en güçlü özelliklerinden biri olan üreteçlerdir (generator).
Yinelenebilir nesneler ve yineleyiciler
Yinelenebilir nesne (iterable), yineleyici verebilen herhangi bir nesnedir: __iter__() metodu vardır (listeler, karakter dizileri, sözlükler, kümeler, dosyalar, range). Yineleyici (iterator) ise değerleri gerçekten tek tek veren nesnedir: __next__() metodu sıradaki değeri döndürür, değerler bitince de StopIteration istisnasını fırlatır. Yerleşik iter(x) ve next(it) fonksiyonları yalnızca bu iki metodu çağırır.
for'un bizim için yaptığını elle yapalım. iter() listeden bir yineleyici ister, her next() de onu bir adım ilerletir. Değerler bitince yineleyici özel bir değer döndürmez; **StopIteration** fırlatır:
colors = ['red', 'green', 'blue']
it = iter(colors)
print(type(it).__name__)
print(next(it))
print(next(it))
print(next(it))
try:
next(it)
except StopIteration:
print('StopIteration: the iterator is exhausted')▸ Beklenen çıktı
list_iterator red green blue StopIteration: the iterator is exhausted
Yani for döngüsü aslında StopIteration görünene kadar next() çağıran bir while döngüsüdür. Aşağıda for'un çalışan bir taklidi var. Karakter dizisini de sözlüğü de aynı şekilde işler, çünkü ikisi de yinelenebilirdir (sözlük dolaşılınca anahtarları gelir):
def my_for(iterable, action):
it = iter(iterable)
while True:
try:
item = next(it)
except StopIteration:
break
action(item)
my_for('abc', print)
my_for({'x': 1, 'y': 2}, print)▸ Beklenen çıktı
a b c x y
Pratikte iki ayrıntı önemlidir. Birincisi, yineleyicinin kendisi de yinelenebilirdir: __iter__() metodu self döndürür; bu yüzden for x in it yazabilirsin. İkincisi, yineleyici tek kullanımlıktır: yalnızca ileri gider ve başa sarılamaz. Liste her istendiğinde yeni bir yineleyici verir; zip, map, filter, dosyalar ve üreteçler ise zaten birer yineleyicidir:
pairs = zip(['Aysel', 'Murad'], [91, 78])
print(list(pairs))
print(list(pairs))
nums = [1, 2, 3]
print(iter(nums) is iter(nums))
it = iter(nums)
print(iter(it) is it)▸ Beklenen çıktı
[('Aysel', 91), ('Murad', 78)]
[]
False
TrueKendi yineleyici sınıfın
__iter__ ve __next__ metotlarına sahip her sınıf for döngüsünde, list() ve sum() fonksiyonlarında, açma (unpacking) işleminde; kısacası yinelenebilir nesne kabul eden her yerde çalışır. İşte bir geri sayım:
class Countdown:
def __init__(self, start):
self.current = start
def __iter__(self):
return self
def __next__(self):
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for n in Countdown(3):
print(n)
print(list(Countdown(5)))▸ Beklenen çıktı
3 2 1 [5, 4, 3, 2, 1]
Kod çalışıyor ama basit bir fikir için fazla “tören” gerektiriyor: durumu self.current içinde elle saklamak ve StopIteration'ı kendimiz fırlatmak zorunda kaldık. Üreteçler aynı işi çok daha az kodla yapar.
Üreteçler: duraklayabilen fonksiyonlar
İçinde **yield anahtar sözcüğü bulunan fonksiyon bir üreteç fonksiyonudur (generator function). Onu çağırmak gövdesini çalıştırmaz; fonksiyon bir üreteç nesnesi** döndürür ve bu nesne bir yineleyicidir. Her next() gövdeyi bir sonraki yield'e kadar çalıştırır, değeri verir ve fonksiyonu tüm yerel değişkenleriyle birlikte dondurur. Sonraki next() tam kaldığı yerden devam eder. Fonksiyon bitince üreteç StopIteration'ı kendiliğinden fırlatır.
def countdown(start):
print('start')
while start > 0:
yield start
start -= 1
print('done')
gen = countdown(2)
print(type(gen).__name__)
print(next(gen))
print(next(gen))
print(next(gen, 'no more values'))▸ Beklenen çıktı
generator start 2 1 done no more values
start, countdown(2) çağrıldığında değil, yalnızca ilk next()'te yazdırıldı. next(gen, default) ise StopIteration yerine varsayılan değeri döndürür.Bunu Countdown sınıfıyla karşılaştır: aynı davranış beş satırda ve durum (start) sıradan bir yerel değişken. Üreteç fonksiyonu çağrıldığında hiçbir şey çalışmadığı için içindeki hatalar da ancak ilk next()'te ortaya çıkar.
Tembel değerlendirme: istendikçe değer
Üreteçler tembeldir (lazy): bir değeri ancak biri onu istediğinde hesaplar. Üreteç ifadesi, yani yuvarlak parantez içindeki üreteç, liste üretecinin tembel ikizidir. Aşağıdaki liste bir milyon sayı saklar (64 bitlik bir bilgisayarda yaklaşık 8 MB); üreteç nesnesi ise dizi ne kadar uzun olursa olsun yalnızca 200 bayt kadar yer kaplar. Ama unutma: tek kullanımlıktır.
import sys
squares_list = [n * n for n in range(1_000_000)]
squares_gen = (n * n for n in range(1_000_000))
print(sys.getsizeof(squares_list) > 1_000_000)
print(sys.getsizeof(squares_gen) < 500)
print(sum(squares_gen))
print(sum(squares_gen))▸ Beklenen çıktı
True True 333332833333500000 0
| Özellik | Liste üreteci [...] | Üreteç ifadesi (...) |
|---|---|---|
| Ne zaman hesaplanır | hepsi hemen | her eleman istendiğinde |
| Bellek | tüm elemanlar | bir seferde bir eleman |
| Yeniden dolaşma | istediğin kadar | yalnızca bir kez |
len() ve indeksleme | var | yok |
| Sonsuz dizi | imkânsız | mümkün |
Tembellik iki güçlü kalıbı mümkün kılar. Sonsuz bir üreteç tamamen normaldir; yeter ki ondan ihtiyacın kadar değer al (itertools.islice ilk n değeri alır). Üreteçlerden oluşan bir zincir ise bir işlem hattı (pipeline) kurar: her aşama bir öncekinden her seferinde bir eleman çeker; bu yüzden gigabaytlarca büyüklükteki bir dosya bile neredeyse sabit bellekle satır satır işlenir:
from itertools import islice
def naturals():
n = 1
while True:
yield n
n += 1
evens = (n for n in naturals() if n % 2 == 0)
print(list(islice(evens, 5)))
with open('server.log', 'w', encoding='utf-8') as f:
f.write('INFO start\nERROR disk full\nINFO ok\nERROR timeout\nWARN slow\n')
with open('server.log', encoding='utf-8') as f:
lines = (line.rstrip('\n') for line in f)
errors = (line for line in lines if line.startswith('ERROR'))
messages = (line.split(' ', 1)[1] for line in errors)
for msg in messages:
print(msg)▸ Beklenen çıktı
[2, 4, 6, 8, 10] disk full timeout
yield from ve çift yönlü üreteçler
yield from iterable, başka bir yinelenebilir nesnenin (başka bir üreteç de olabilir) tüm değerlerini tek tek aktarır. Özyineleme için doğal bir araçtır; örneğin her derinlikteki iç içe listeleri “düzleştirmek” için:
def flatten(items):
for item in items:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
data = [1, [2, 3, [4, 5]], [], [[6]], 7]
print(list(flatten(data)))▸ Beklenen çıktı
[1, 2, 3, 4, 5, 6, 7]
yield from aynı zamanda bir ifadedir: değeri, alt üretecin return ile döndürdüğü değerdir. yield'in kendisi de bir ifadedir: generator.send(value) üreteci devam ettirir ve yield ifadesi o değeri alır. Böylece üreteç yalnızca veri üretmekle kalmaz, veri de alabilir. İlk send()'den önce üreteç next() ile ilk yield'e kadar ilerletilmelidir:
def numbers(values):
total = 0
for v in values:
yield v
total += v
return total
def pipeline():
subtotal = yield from numbers([1, 2, 3])
print('subtotal:', subtotal)
yield from 'ab'
print(list(pipeline()))▸ Beklenen çıktı
subtotal: 6 [1, 2, 3, 'a', 'b']
def running_average():
total, count = 0, 0
average = None
while True:
value = yield average
total += value
count += 1
average = total / count
avg = running_average()
next(avg)
print(avg.send(10))
print(avg.send(20))
print(avg.send(60))▸ Beklenen çıktı
10.0 15.0 30.0
total, count) çağrılar arasında saklar; bunun için sınıf yazmaya gerek kalmadı.Bir diziyi size uzunluğunda parçalara bölen ve bunları tek tek veren bir chunks(items, size) üreteci yaz (son parça daha kısa olabilir). Dilimleme sayesinde hem listeyle hem karakter dizisiyle çalışmalıdır.
def chunks(items, size):
# yield slices of `size` items; the last one may be shorter
...
print(list(chunks([1, 2, 3, 4, 5, 6, 7], 3)))
print(list(chunks('abcde', 2)))▸ Beklenen çıktı
[[1, 2, 3], [4, 5, 6], [7]] ['ab', 'cd', 'e']
Fibonacci sayılarını (0, 1, 1, 2, 3, 5, …) sonsuza kadar veren bir fibonacci() üreteci yaz ve islice ile ilk 10 tanesini yazdır.
from itertools import islice
def fibonacci():
# yield 0, 1, 1, 2, 3, 5, ... forever
...
print(list(islice(fibonacci(), 10)))▸ Beklenen çıktı
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Önemli noktalar
forbir keziter()çağırır, ardındanStopIterationgörünene kadarnext()çağırır.- Yinelenebilir nesnenin
__iter__'i, yineleyicinin ayrıca__next__'i vardır; yineleyici tek kullanımlıktır. yieldiçeren fonksiyon çağrıldığında bir üreteç döndürür; gövde tembelce, heryield'de duraklayarak çalışır.- Üreteç ifadesi
( ... )sabit bellek kullanır; liste üreteci[ ... ]ise tüm listeyi oluşturur. yield fromişi başka bir yinelenebilir nesneye devreder ve alt üretecinreturndeğerini alır.
Kendini test et
10 soru. Her doğru cevap XP kazandırır.
next(it) çağrılınca ne olur?