İçeriğe geç
Educora
İleri22 dk15 / 42

Yineleyiciler ve üreteçler

`for` döngüsü aslında nasıl çalışır: yineleme protokolü, `__iter__`/`__next__`, `yield` ile üreteçler, tembel değerlendirme, `yield from` ve belleği az kullanan işlem hatları.

Kendini test et
Bu derste öğreneceklerin
  • Yineleme protokolünü (iter, next, StopIteration) açıklamak ve for döngüsünün içinde ne olduğunu anlatmak
  • __iter__ ve __next__ ile bir yineleyici sınıfı, yield ile bir üreteç fonksiyonu yazmak
  • Tembel değerlendirmeyle sonsuz diziler ve işlem hatları kurmak
  • yield from ve send() ö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

Tanım
Yinelenebilir nesne ve yineleyici

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:

Python
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):

Python
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:

Python
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
True

Kendi 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:

Python
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.

Python
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
Dikkat: 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.

Python
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
ÖzellikListe üreteci [...]Üreteç ifadesi (...)
Ne zaman hesaplanırhepsi hemenher eleman istendiğinde
Bellektüm elemanlarbir seferde bir eleman
Yeniden dolaşmaistediğin kadaryalnızca bir kez
len() ve indekslemevaryok
Sonsuz diziimkânsızmü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:

Python
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
Dosya nesnesi zaten satırlar üzerinde bir yineleyicidir; üç üreteç ifadesi dosyayı hiçbir zaman tamamen belleğe yüklemeden süzer ve dönüştürür.

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:

Python
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:

Python
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']
Python
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
Üreteç durumunu (total, count) çağrılar arasında saklar; bunun için sınıf yazmaya gerek kalmadı.
Alıştırma

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.

Alıştırma · Python
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']
Alıştırma

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.

Alıştırma · Python
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

  • for bir kez iter() çağırır, ardından StopIteration görünene kadar next() çağırır.
  • Yinelenebilir nesnenin __iter__'i, yineleyicinin ayrıca __next__'i vardır; yineleyici tek kullanımlıktır.
  • yield içeren fonksiyon çağrıldığında bir üreteç döndürür; gövde tembelce, her yield'de duraklayarak çalışır.
  • Üreteç ifadesi ( ... ) sabit bellek kullanır; liste üreteci [ ... ] ise tüm listeyi oluşturur.
  • yield from işi başka bir yinelenebilir nesneye devreder ve alt üretecin return değerini alır.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
Tükenmiş bir yineleyicide next(it) çağrılınca ne olur?