2.31. Iteraattorit ja generaattorit

for -silmukka on tehnyt enemmän työtä kuin miltä näyttää. Tämä sivu käsittelee iteraattoriprotokollaa, jonka varassa se toimii, sekä yield -avainsanaa, jonka avulla voit rakentaa omia iteraattoreitasi.

2.31.1. Iteraattoriprotokolla

Jokainen olio, jonka yli voidaan iteroida, toteuttaa kaksi metodia:

  • __iter__() – palauttaa iteraattorin olion alkioiden yli.

  • __next__() – iteraattorissa palauttaa seuraavan alkion tai nostaa poikkeuksen StopIteration, kun niitä ei ole enää.

Sisäänrakennettu iter() kutsuu metodia __iter__; next() kutsuu metodia __next__. Käy listaa läpi käsin:

it = iter([10, 20, 30])
print(next(it))    # 10
print(next(it))    # 20
print(next(it))    # 30
print(next(it))    # raises StopIteration
for-silmukka kutsuu __iter__ kerran saadakseen iteraattorin, sitten kutsuu __next__ toistuvasti, kunnes StopIteration päättää silmukan.

for on syntaksisokeria ilmaisulle ”kutsu __iter__ kerran, sitten silmukoi metodia __next__, kunnes StopIteration”.

Mitä for x in items: todellisuudessa tekee:

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

Jokainen lista, monikko, merkkijono, sanakirja, joukko, tiedosto-olio ja generaattori toteuttaa jo metodit __iter__ ja __next__ – minkä vuoksi ne kaikki toimivat for -silmukan kanssa.

2.31.2. yield ja generaattorifunktiot

Funktio, joka sisältää yield -lauseen, on generaattorifunktio. Sen kutsuminen ei suorita runkoa; se palauttaa generaattoriolion (iteraattorin), joka suorittaa rungon yhden yield -kohdan kerrallaan:

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

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

Tuloste:

0
1
2

Jokainen kutsu funktioon next() jatkaa funktiota seuraavaan yield -kohtaan asti, luovuttaa tuon arvon kutsujalle ja keskeytyy siihen. Paikallinen tila (tässä tapauksessa i) säilyy jatkamisten välillä.

Sekvenssikaavio kahdella elämänviivalla (kutsuja ja generaattorirunko). Kutsuja kutsuu count_up_to(3), joka luo generaattorin suorittamatta runkoa. Jokainen seuraava next() suorittaa rungon seuraavaan yield-kohtaan asti, palauttaa tuotetun arvon ja keskeytyy. Neljäs next() putoaa rungon lopusta ja nostaa StopIteration-poikkeuksen. Muuttuja i säilyy keskeytysten yli.

next() suorittaa rungon seuraavaan yield -kohtaan asti, luovuttaa arvon takaisin ja keskeytyy. Paikallinen tila säilyy keskeytyksen yli.

Generaattorit ovat helpoin tapa tuottaa sekvenssi laiskasti – listaa ei rakenneta, alkiot lasketaan vain kun kuluttaja niitä pyytää, ja funktio voi tuottaa alkioita loputtomiin niin halutessaan.

2.31.3. Laiskat liukuhihnat

Generaattorit kytkeytyvät hyvin yhteen. Yhden generaattorin tuloste voi syöttää toista:

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)

Arvot virtaavat liukuhihnan läpi yksi kerrallaan – ei välivaiheen listaa, ei muuttujaan numbers sisäänrakennettua ylärajaa, ja kuluttaja (for v in pipeline) päättää milloin lopettaa.

Kolme laatikkoa vasemmalta oikealle: numbers(), squares(source) ja for-v-in-pipeline -kuluttaja. Alle on piirretty kolme kiertoa. Kullakin kierroksella kuluttaja lähettää vetopyynnön vasemmalle squares-funktiolle, joka lähettää vetopyynnön vasemmalle numbers-funktiolle; numbers tuottaa arvon oikealle squares-funktiolle, joka tuottaa neliöidyn arvonsa oikealle kuluttajalle.

Jokainen next() kuluttajassa laukaisee yhden vedon ketjun läpi; arvot ovat olemassa vain kun jokin niitä pyytää.

2.31.3.1. yield from

Silmukka, joka vetää alkioita toisesta iteroitavasta ja tuottaa jokaisen niistä, on riittävän yleinen, että Python tarjoaa sille oikotien. Lauseke yield from iter tuottaa jokaisen arvon, jonka iteroitava tuottaa, järjestyksessä – aivan kuin generaattorissa olisi sisäänkirjoitettu for x in iter: yield x -silmukka:

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

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

Tuloste:

1
2
3
4
5
a
b
c

yield from on täysin samanarvoinen kuin eksplisiittinen for -silmukka, vain lyhyempi, ja se välittää poikkeuksen StopIteration sisemmästä iteroitavasta ulompaan generaattoriin siististi – hyödyllistä ketjutettaessa useita generaattoreita peräkkäin.

2.31.3.2. Kun yield loppuu

Generaattorifunktion lopusta putoaminen (tai eksplisiittiseen return -lauseeseen osuminen) nostaa poikkeuksen StopIteration automaattisesti. Sitä ei tarvitse nostaa käsin; ympäröivä for -silmukka näkee sen ja päättyy.

Käytä generaattoreita, kun tuottava koodi kirjoitetaan luontevasti silmukkana, jossa on muutama yield-kohta; käytä tavallista listamuodostinta, kun todella tarvitset koko sekvenssin muistissa.