2.31. Iteratori i generatori

for petlja obavljala je više posla nego što se čini. Ova stranica pokriva protokol iteratora na kojem se izvodi te ključnu riječ yield koja vam omogućuje izgradnju vlastitih iteratora.

2.31.1. Protokol iteratora

Svaki objekt koji se može iterirati implementira dvije metode:

  • __iter__() – vraća iterator nad stavkama objekta.

  • __next__() – na iteratoru, vraća sljedeću stavku ili podiže StopIteration kada ih više nema.

Ugrađena funkcija iter() poziva __iter__; next() poziva __next__. Prođite kroz listu ručno:

it = iter([10, 20, 30])
print(next(it))    # 10
print(next(it))    # 20
print(next(it))    # 30
print(next(it))    # raises StopIteration
for petlja jednom poziva __iter__ kako bi dobila iterator, zatim opetovano poziva __next__ sve dok StopIteration ne završi petlju.

for je sažeti zapis za „jednom pozovi __iter__, zatim petlji pozivaj __next__ sve do StopIteration.”

Što for x in items: zapravo radi:

_it = iter(items)
while True:
    try:
        x = next(_it)
    except StopIteration:
        break
    # ... loop body ...

Svaka lista, n-torka, niz znakova, rječnik, skup, objekt datoteke i generator već implementiraju __iter__ i __next__ – zbog čega svi rade s for.

2.31.2. yield i funkcije generatora

Funkcija koja sadrži naredbu yield je funkcija generatora. Njezin poziv ne izvodi tijelo; ona vraća objekt generatora (iterator) koji izvodi tijelo jedan yield po jedan:

def count_up_to(n):
    i = 0
    while i < n:
        yield i
        i += 1

for value in count_up_to(3):
    print(value)

Izlaz:

0
1
2

Svaki poziv funkcije next() nastavlja funkciju do sljedećeg yield, predaje tu vrijednost pozivatelju i zaustavlja se tamo. Lokalno stanje (i u ovom slučaju) čuva se između nastavaka.

Sekvencijski dijagram s dvije linije života (pozivatelj i tijelo generatora). Pozivatelj poziva count_up_to(3), što stvara generator bez izvođenja tijela. Svaki sljedeći next() izvodi tijelo do sljedećeg yield, vraća ustupljenu vrijednost i zaustavlja se. Tijekom četvrtog next() izvođenje izlazi s kraja i podiže StopIteration. Varijabla i čuva se kroz zaustavljanja.

next() izvodi tijelo do sljedećeg yield, vraća vrijednost i zaustavlja se. Lokalno stanje preživljava zaustavljanje.

Generatori su najlakši način za lijeno (lazy) proizvođenje niza – nikakva lista se ne gradi, stavke se izračunavaju tek kada ih potrošač zatraži, a funkcija može ustupati stavke zauvijek ako želi.

2.31.3. Lijeni cjevovodi (pipelines)

Generatori se dobro slažu. Izlaz jednog generatora može hraniti drugi:

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

def squares(source):
    for x in source:
        yield x * x

pipeline = squares(numbers())

for v in pipeline:
    if v > 100:
        break
    print(v)

Vrijednosti teku kroz cjevovod jedna po jedna – bez međuliste, bez ugrađene gornje granice u numbers te potrošač (for v in pipeline) odlučuje kada stati.

Tri okvira slijeva nadesno: numbers(), squares(source) i potrošač for-v-in-pipeline. Tri ciklusa nacrtana su ispod. U svakom ciklusu potrošač šalje zahtjev za povlačenjem ulijevo prema squares, koji šalje zahtjev za povlačenjem ulijevo prema numbers; numbers ustupa vrijednost udesno prema squares, koji ustupa svoju kvadriranu vrijednost udesno prema potrošaču.

Svaki next() na potrošaču pokreće jedno povlačenje kroz lanac; vrijednosti postoje samo kada ih nešto zatraži.

2.31.3.1. yield from

Petlja koja povlači stavke iz drugog iterabilnog objekta i ustupa svaku od njih dovoljno je česta da Python pruža prečac. Izraz yield from iter ustupa svaku vrijednost koju iterabilni objekt proizvede, redom – kao da generator ima ugrađenu for x in iter: yield x petlju:

def chain(*sources):
    for source in sources:
        yield from source

for v in chain([1, 2, 3], (4, 5), "abc"):
    print(v)

Izlaz:

1
2
3
4
5
a
b
c

yield from je potpuno ekvivalentan eksplicitnoj for petlji, samo kraći, i čisto prosljeđuje StopIteration iz unutarnjeg iterabilnog objekta prema vanjskom generatoru – korisno pri ulančavanju nekoliko generatora od kraja do kraja.

2.31.3.2. Kada yield ponestane

Izlazak s kraja funkcije generatora (ili nailazak na eksplicitni return) automatski podiže StopIteration. Nema je potrebe podizati ručno; okolna for petlja je vidi i završava.

Koristite generatore kada je proizvodni kôd prirodno napisan kao petlja s nekoliko točaka yield; koristite običnu listnu komprehenziju kada vam je doista potreban cijeli niz u memoriji.