Ohjelmoinnin peruskurssi Y2, kurssimateriaali

4.3. Läpikäynnit, iteraattorit, generaattorit

«  4.2. Tulostus tekstitiedostoon   ::   Etusivulle   ::   4.4. Tehtävä: Reading a chunked file  »

4.3. Läpikäynnit, iteraattorit, generaattorit

Esitietokurssilla CSE-A1111 Ohjelmoinnin peruskurssi Y1 tutustuttiin for- ja while-silmukoihin sekä merkkijonojen, listojen ja sanakirjojen läpikäyntiin (ks. Y1-kurssin kurssimoniste).

Tässä luvussa perehdymme aiheeseen tarkemmin. Tutustumme myös erilaisiin tapoihin käydä läpi tietorakenteita, iteraattoreihin ja generaattoreihin. Näitä käsitellään Pythonin dokumentaatiossa monessa kohtaa:

Iteraattorit

Peruskurssilla ja tälläkin kurssilla olemme käyneet läpi merkkijonoja, listoja, monikoita ja sanakirjoja for-silmukalla. Katsotaan kertaukseksi muutama esimerkki:

>>> l = ['a', 'b', 'c']
>>> for x in l:
...     print(x)
...
a
b
c
>>> for x in range(1, 11, 2):
...     print(x)
...
1
3
5
7
9
>>> for x in "hijklmno":
...     print(x)
...
h
i
j
k
l
m
n
o
>>> for x in {'name': 'Pekka', 'age': 32}:
...     print(x)
...
age
name
>>>

Pythonin dokumentaatiossa 8.3. The for statement todetaan:

The for statement is used to iterate over the elements of a sequence (such as a string, tuple or list) or other iterable object:

for_stmt ::= "for" target_list "in" expression_list ":" suite
["else" ":" suite]

The expression list is evaluated once; it should yield an iterable object. An iterator is created for the result of the expression_list. The suite is then executed once for each item provided by the iterator, in the order returned by the iterator.

Iterable

Esimerkissä tällaisia iterable objecteja olivat lista, range, merkkijono ja sanakirja. Tyypillisesti erilaiset säiliöluokat ovat iterableja. Ollakseen iterable, täytyy oliolla olla metodi __iter__, joka palauttaa iteraattorin.

Iteraattori

Mikä tuo iteraattori sitten on? Iteraattori on olio, joka suorittaa varsinaisen läpikäynnin. Jotta olio olisi iteraattori, on sillä oltava metodi __next__. Listan tapauksessa __next__ palauttaa yksi kerrallaan listan alkiot (esim. for-silmukan käyttöön) ja kun kaikki alkiot on palautettu, nostaa poikkeuksen StopIteration. For-silmukka osaa lopettaa nähdessään tämän poikkeuksen.

Metodia __next__ voi toki kutsua suoraankin ilman for-silmukkaa. palautettava oletusarvo.

>>> l=list(range(3))
>>> it = l.__iter__()
>>> it.__next__()
0
>>> it.__next__()
1
>>> it.__next__()
2
>>> it.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Iteraattorien käyttöä varten on kaksi valmiiksi määritettyä funktiota next ja iter, jotka helpottavat toisinaan iteraattorien käyttöä.

Generaattori

Generaattori on kätevä tapa toteuttaa iteraattori. Generaattori näyttää tavalliselta funktiolta, mutta sisältää koodissaan jossain kohtaa lauseen (tai lausekkeen) yield. Esim.

>>> def squares(start, end):
...     i = start
...     while i < end:
...         yield i ** 2
...         i = i + 1
...
>>>
>>> for x in squares(0, 10):
...     print(x)
...
0
1
4
9
16
25
36
49
64
81
>>>
For-silmukan aluksi kutsutaan funktiota squares. Sen suoritys etenee, kunnes kohdataan yield-lause.
yield palauttaa i:n toisen potenssin for-silmukalle, mutta funktion squares suoritus ei lopu.
Muuttujaan x sidotaan yieldin palauttama arvo.
For-silmukan runko suoritetaan ja tulostetaan x:n arvo.
Suoritus palaa takaisin funktioon squares ja jatkuu kohdasta i = i + 1.
Kun while-silmukan ehto tulee epätodeksi, loppuu funktion suoritus ja kontrolli siirtyy for-lauseen jälkeiseen kohtaan ohjelmassa.

Päättymätön iteraattori

Toisinaan on kätevää tehdä iteraattorista tai generaattorista päättymätön, eli että se ei koskaan lopeta toimintaansa. Iteroinnin lopettamisesta pitää tällöin huolta iteraattorin kutsuja. Esimerkiksi:

>>> def repeat(x):
...     while True:
...         yield x
...
>>> r=repeat(True)
>>> next(r)
True
>>> next(r)
True
>>> next(r)
True
>>> next(r)
True
>>> for (i, x) in zip(range(4), repeat('huhuu')):
...    print('{}: {}'.format(i, x))
...
0: huhuu
1: huhuu
2: huhuu
3: huhuu
>>>
Silmukka ei pääty koskaan, jollei repeatin kutsuja lopeta sitä.
Tehdään repeat-iteraattori ja kutsutaan sitä suoraan funktion next avulla. Tätä voitaisiin jatkaa loputtomasti.
Yhdistetään kaksi iteraattoria range(4) ja repeat('huhuu') Pythonin valmiilla funktiolla zip. Se tuottaa iteraattorin, joka palauttaa pareittain näiden kahden iteraattorin (tai oikeammin iterablen iteraattorin) palauttamat arvot.
Koska range:n iteraattori ei ole päättymätön, for-silmukan suoritus ei jatku loputtomasti.

Moduli itertools

Pythonin kirjastossa on moduli itertools (ks 10.1. itertools — Functions creating iterators for efficient looping). Se määrittää joukon valmiita iteraattoreita, joita yhdistelemällä saa näppärästi ratkottua monia läpikäynnin ongelmia ilman, että itse tarvitsee toteuttaa iteraattoria. Moduliin kannattaa tuotustua tarkemminkin, mutta katsotaan tässä paria esimerkkiä.

count

Hieman kuin range, mutta ei ylärajaa. count() tuottaa numerot 0, 1, 2, jne. . Parametrina voi antaa alkukohdan ja askelen. Esim. count(1, 3) tuottaa numerot 1, 4, 7, 10, jne.

cycle

Tuottaa uudestaan ja uudestaan parametrina annetun iterablen tuottamat arvot. Esim. cycle(range(3)) tuottaa loputtomasti arvot 0, 1, 2, 0, 1, 2, jne.

repeat

Yllä määrittämämme repeat löytyy valmiina.

Esimerkkejä

Tulostetaan annetun vuoden kaikki arkipäivät:

from itertools import count
from datetime import timedelta, date

def weekdays(start):
    return filter(lambda d: d.weekday() < 5, ((start + timedelta(days=i)) for i in count()))


def print_weekdays(year):
    for d in weekdays(date(year, 1, 1)):
        if d.year > year:
            break
        else:
            print(d)

Joukko reseptejä

Modulin itertools iteraattorien käytöstä löytyy valmiita reseptejä dokumentaation kohdasta 10.1.2. Itertools Recipes.

«  4.2. Tulostus tekstitiedostoon   ::   Etusivulle   ::   4.4. Tehtävä: Reading a chunked file  »