Loeng 7: Generaatorid, iteraatorid ja laisk itereerimine, lausendid yield
ja yield from
, sisseehitatud funktsioonid iter
ja next
, generaatori lühiesitus¶
Koodinäited¶
Generaator ja lausend yield
¶
Generaator on spetsiaalne funktsioon, mille nimeruum säilib väärtuse väljastamisel. Lisaks säilivad ka programmi voo andmed, neid andmeid kasutatakse järgmiste tulemuste genereerimiseks. Generaator väljastab väärtusi (iteraate) erilisel viisil $-$ itereerides neid laisalt. Generaatorfunktsioon väljastab väärtusi ükshaaval salvestamata suuri andmestruktuure (andmetüübid: loend, korteež, hulk, sõnaraamat, jne.) masina mällu. Väärtuse väljastamiseks kasutame generaatorfunktsiooni blokis lausendit yield
. Tuletame meelde, et tavalises Pythoni funktskoonis väljastasime tulemusi kasutades lausendidt return
.
Generaatori abil saame defineerida või luua iteraatori. Eelnevalt olema kasutanud for
ja while
tsükleid, et itereerida üle iteraatortoega objektide. Lisaks iteraatortoega objektidele ja andmetüüpidele saame itereerida üle iteraatorite ja generaatorite. Iteraator on objekt üle mille saab itereerida.
Defineerime generaatori ja itereerime üle selle:
def int_gen(): # Defineerime generaatorfunktsiooni.
yield 1 # Väljastame arvu 1, yielding a value.
yield 2 # Väljastame arvu 2.
for i in int_gen(): # Itereerin üle generaatori.
print(i) # NB! Operaator in loob taustal iteraatori.
1 2
Defineerime generaatori, loome selle abil iteraatori ja itereerime üle selle:
def int_gen():
yield 1
yield 2
iteraator = int_gen() # NB! Loome iteraatori nimega 'iteraator'.
for i in iteraator: # Itereerin üle iteraatori.
print(i)
iteraator2 = int_gen() # Uue iteraatori loomine kasutades sama generaatorit.
for i in iteraator2: # Itereerin üle teise iteraatori.
print(' ', i)
1 2 1 2
Generaatorfunktsioonile saame määrata argumente ja edastada argumentide väärtusi. Siin kehtivad kõik meile teadaolevad def
bloki reeglid ja võimalused.
def int_gen(start, stop): # Argumentide lisamine.
for i in range(start, stop):
yield i
for i in int_gen(1, 3): # Edastan argumentide väärtused.
print(i)
1 2
def int_gen(*args): # Args-tüüpi argumentide lisamine.
for i in args:
yield i
iter1 = int_gen('Python', 'on', 'madu.') # Lisa meelevaldne arv väärtusi.
for i in iter1:
print(i, end=' ')
Python on madu.
Kontrollime mis andmetüüpidega ja objektidega on tegemist:
print(type(int_gen(1, 2, 3)))
obj = int_gen(1, 2, 3) # Loome generaatori objekti ehk iteraatori.
print(obj) # Generaatori objekt on iteraator.
<class 'generator'> <generator object int_gen at 0x14484fe00>
Sisseehitatud funktsioon next
(built-in function next
)¶
Iteraatori järgmise väärtuse saab välja kutsuda kasutades sisseehitatud funktsiooni next
:
def int_gen():
for i in [1, 2, 3, 4, 5]:
yield i
g = int_gen() # Loome iteraatori g.
next(g) # Rakenda funktsiooni next iteraatorile.
1
next(g) # Mäletab eelmist olekut.
2
next(g) # Järgmine iteraat, jne.
3
Funktsiooni next
pole mõtet rakendada otse generaatorile, antud juhul funktsioon next
ei väljasta järgmist iteraati (lisaks vt. selle dokumendi lõppu):
print(next(int_gen()))
print(next(int_gen())) # NB! Ei itereeri, järgmist iteraati ei väljastata.
g2 = int_gen() # Loon uue iteraatori g2.
i = 1
while i < 4:
print(next(g2), end=' ')
i += 1
1 1 1 2 3
Erisus StopIteration
¶
Kui iteraator on ammendunud siis tõstatakse erisus StopIteration
. Pythoni erisusestest ja veateadetest räägme tuleviku loengutes üksikasjalikumalt.
def int_gen2():
for i in [1, 2]:
yield i
g = int_gen2()
next(g)
1
next(g)
2
next(g) # Järgmist väärtust pole olemas, iteraator ammendus.
--------------------------------------------------------------------------- StopIteration Traceback (most recent call last) Cell In[12], line 1 ----> 1 next(g) StopIteration:
Laisk itereerimine, mõiste¶
Generaatori abil loodud iteraatorit nimetatakse laisaks iteraatoriks. Laisad iteraatorid lubavad töödelda suuri koguseid andmeid ilma neid korraga salvestamata. Nagu mainiti loengu alguses, generaatorid ja iteraatorid väljatavad tulemusi, vajadusel kasvõi lõpmatult kaua või lõpmatu arv kordi, ainult siis kui seda nendelt küsitakse. Tihti polegi võimalust salvastada kõiki vajaminevaid väärtusi. Nt. arvuti parooli kombinatsioonide arv. Tähemärkide ja numbrite kõikvõimalike lõpliku pikkusega permutatsioonide loendit pole võimalik arvutisse salvestada. Neid on kasulik ükshaaval luua.
Lõpmatute generaatorite näited:
def seitse():
while True: # Lõpmatu tsükkel.
yield 7
s = seitse() # Loon lõpmatu iteraatori.
i = 0
while i < 3: # Peatan itereerimise käsitsi.
print(i * ' ', next(s)) # Väärtuse loomine ja väljastamine.
i += 1
7 7 7
def arvud(n = 0): # Vaikimisi alustame 0-st.
while True: # Lõpmatu tsükkel.
yield n
n += 1
num = arvud() # Loome iteraatori num.
a = [next(num) for _ in range(5)] # Loendi defineerimine funktsiooni next abil.
b = [next(num) for _ in range(5)] # Sama iteraator, mäletab viimast iteraatori olekut.
print(a, '-->', b)
[0, 1, 2, 3, 4] --> [5, 6, 7, 8, 9]
Iteraatortoega objektide konstruktorid ja generaator¶
Pythoni sisseehitatud andmetüüpide kostruktorfunktsioonid list
, tuple
, set
ja dict
itereerivad taustal üle generaatorite ja iteraatorite:
def int_gen3():
for i in [1, 2, 3]:
yield i
g = int_gen3()
print(list(int_gen3())) # Taustal funk. list itereerib üle generaatori.
print(list(g)) # Taustal funk. list itereerib üle iteraatori.
[1, 2, 3] [1, 2, 3]
Sarnaselt käitub ka sisseehitatud funktsioon range
. Kuid pea meeles, et vahemik range
pole tehniliselt ei iteraator ega ka generaator. Seega vahemik range
võib käituda ebaootuspäraselt:
list(range(4)) # NB! Vahemik range ei ole tehniliselt iteraator ega generaator aga käitub sarnaselt eelmisele rakule.
[0, 1, 2, 3]
next(range(3)) # NB! Vahemikule range pole võimalik rakendada funktsiooni next.
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[17], line 1 ----> 1 next(range(3)) TypeError: 'range' object is not an iterator
Proovi ka muid sisseehitatud funktsioone mida saab rakendada iteraatortoega objektidele, nt. sum
:
def int_gen4():
for i in [1, 2, 3]:
yield i
gen = int_gen3()
print(sum(int_gen3())) # Taustal itereerib üle generaatori.
print(sum(gen)) # Taustal itereerib üle iteraatori.
6 6
Sisseehitatud funktsioon iter
¶
Funktsioon iter
loob iteraatori kasutades iteraatortoega objekte ehk teisendab iteraatortoega objekte iteraatoriks. Näide kasutades iteraatortoega loendit:
lst = [1, 2, 3] # Proovi ka teisi andmetüüpe.
iter_lst = iter(lst) # Funktsiooni iter rakendamine loendile.
print(type(iter_lst))
print(next(iter_lst), next(iter_lst))
<class 'list_iterator'> 1 2
for i in iter_lst:
print(i) # Mäletab eelmist olekut.
3
Veel üks näide, kasutades iteraatortoega sõnet:
st = 'Python'
iter_st = iter(st)
print(type(iter_st))
print(next(iter_st), next(iter_st))
<class 'str_ascii_iterator'> P y
for i in iter_st:
print(i, end=' ') # Mäletab eelmist olekut.
t h o n
Rekursiivne generaator¶
Nii nagu oli tavaliste Pythoni funktsioonidega saame ka siin kasutada rekursiooni:
def infinity(start):
yield start
for x in infinity(start + 1): # Viitan generaatorile endale.
yield x
inf = infinity(0) # Iteraatori loomine.
i = 0
while i < 3: # Peatan itereerimise käsitsi.
print(next(inf))
i += 1
0 1 2
Lausend yield from
¶
Alltoodud näidet kasutame allpool lausendi yield from
seletamiseks. Uurime näidet:
def generator1():
for i in range(3):
yield 'Gen 1:', i
def generator2():
for i in range(3, 6):
yield '\tGen 2:', i
def generator():
for i in generator1(): # Itereerin üle generaatori ja LEGB reegel.
yield i
for i in generator2(): # Itereerin üle generaatori ja LEGB reegel.
yield i
for i in generator():
print(i[0], i[1])
Gen 1: 0 Gen 1: 1 Gen 1: 2 Gen 2: 3 Gen 2: 4 Gen 2: 5
Lausend yield from
võimaldab alamgeneraatori kasutust generaatori sees. Lausend võimaldab eelmist näidet tunduvalt lühendada:
def generator():
yield from generator1()
yield from generator2()
for i in generator():
print(i[0], i[1])
Gen 1: 0 Gen 1: 1 Gen 1: 2 Gen 2: 3 Gen 2: 4 Gen 2: 5
Rekursioon ja lausend yield from
:
def infty(start):
yield start
yield from infty(start + 1) # Rekursiooni loomine.
for i in infty(0): # Itereerime üle generaatori.
if i < 3: # Peatame itereerimise käsitsi.
print(i)
else:
break
0 1 2
def recursive_generator(lst): # Argumendina eeldame listi.
if lst: # Triviaalse objekti booli tõeväärtus on False.
yield lst[0]
yield from recursive_generator(lst[1:]) # Rekursiooni loomine.
for k in recursive_generator([6, 2, 5]):
print(k)
6 2 5
Kõrgemat järku funktsiooni analoog generaatori defineerimisel¶
Generaatori defineerimisel saame argumendina edastada funktsioone või väljastada ehk yield
-da funktsioone. Seega saame defineerida kõrgemat järku generaatorfunktsiooni. Täpselt nii nagu saime seda teha tavaliste Pythoni funktsioonide defineerimisel.
Näide: Logistiline kujutus on antud kujul: $$x_{n+1} = r x_n (1 − x_n),$$ kus $r$ on kontrollparameeter omades väärtusi vahemikus $[0, 4]$, $n \in \mathbb{Z}$ on itereerimisindeks ja iteraat $x_n$ saab omada väärtusi vahemikus $[0, 1]$.
Kasutame ülaltoodud kujutuse funktsiooni kujul: $$f(x) = r x (1 − x),$$ generaatorifunktsiooni argumendi väärtusena ning leiame kujutse iteraadid $x_{n+1} = f(x_n)$ ehk kujutise.
r = 3.7
f = lambda x: r*x*(1 - x)
def iterate_func(func, x): # Itereerin üle funktsiooni.
while True: # Lõpmatu tsükkel.
yield x
x = func(x) # Rakendame funktsiooni func x-le ja kirjutame x väärtuse üle.
iterates_xn = iterate_func(f, 0.6) # Loome iteraatori.
for _ in range(4): # Peatan itereerimise käsitsi.
print(next(iterates_xn))
0.6 0.8880000000000001 0.3679871999999997 0.8605186963537916
Sama näide, aga siin salvestame leitud väärtused loendisse:
x = iterate_func(f, 0.6) # Uus iteraator.
lst = []
for _ in range(4): # Peatan itereerimise käsitsi.
lst.append(next(x)) #Lisan iteraadid algselt tühja listi.
print(lst)
[0.6, 0.8880000000000001, 0.3679871999999997, 0.8605186963537916]
Generaatoravaldis, generaatori väljendid ehk generaatori lühiesitus (generator comprehension)¶
Eelmisel nädalal tutvustatud for
-in
avaldis (list comprehension, set comprehension, dictionary comprehension) lõi listi, hulga või sõnaraamatu mis omakorda hoiab kõiki loodud elemente masina mälus. Nagu juba eespool mainitud, generaator ja ka selle allpool tutvustatud lühiesitus ei loo suurt mälustruktuuri vaid väljastab tulemused laisalt ehk ühekaupa ja vastavalt vajadusele.
Generaatoravaldist defineerime kasutades süntaksis ümaraid sulge. See on ka põhjus miks me eelmine nädal ei defineerinud korteežiavaldist (tuple comprehension). Korteežiavaldist pole olemas kuna see defineerib generaatori.
Näiteid:
ruudud = (i**2 for i in range(4)) # Generaatori loomine kasutades generaatori lühiesitust.
print(type(i**2 for i in range(4))) # Andmetüüp.
print(type(ruudud)) # NB! Esimene rida omistas muutujale generaatori.
print(ruudud) # Generaatori objekt ehk iteraator.
<class 'generator'> <class 'generator'> <generator object <genexpr> at 0x144cc35e0>
list(ruudud) # Transleerime iteraadid listi.
[0, 1, 4, 9]
rd = (i**2 for i in range(3)) # Anname nime generaatorile.
for i in rd: # NB! Itereerin üle generaatori.
print(i)
0 1 4
Nii nagu eelmise nädala loengus filtreerisime liste, hulkasid ja sõnaraamatuid, saame ka generaatoravaldistes kasutada for
-in
-if
avaldist. Näiteks loome generaaatori mis genereerib ainult paarituid ruute:
ptud_ruudud = (i**2 for i in range(7) if i%2) # Sama mis i % 2 == 1.
for i in ptud_ruudud:
print(i)
1 9 25
Pesastatud for
-in
avaldis ja generaatoravaldis, vt. vrd. eelmise nädala materjale:
nested_gen = ((n, m) for n in range(2) for m in range(10, 12))
for j in nested_gen:
print(j)
(0, 10) (0, 11) (1, 10) (1, 11)
Segaarvaldised ja generaatori lühiesitus: if
-else
valikuavaldis ja for
-in
või for
-in
-if
avaldis¶
Näide: Filtreeri loend a
generaatoravaldise abil: Juhul kui arv on suurem või võrdne $50$-ga liida sellele $1$, juhul kui arv on väiksem kui $50$ liida sellele $5$.
a = [22, 13, 45, 50, 98, 69, 43]
arvud = (x + 1 if x >= 50 else x + 5 for x in a)
for i in arvud:
print(i, end=' ')
27 18 50 51 99 70 48
Ekvivalentne kuju kasutades def
-blokki:
def gen(lst):
for i in lst:
if i >= 50:
yield i + 1
else:
yield i + 5
for i in gen(a):
print(i, end=' ')
27 18 50 51 99 70 48
Näide: Filtreeri loend a
generaatoravaldise abil: 1) Elimineeri paaritud arvud. 2) Juhul kui paaris arv on suurem või võrdne $50$-ga liida sellele $1$, juhul kui paaris arv on väiksem kui $50$ liida sellele $5$.
a = [22, 13, 45, 50, 98, 69, 43, 44, 1]
numbrid = (x + 1 if x >= 50 else x + 5 for x in a if x % 2 == 0)
for i in numbrid:
print(i, end=' ')
27 51 99 49
Ekvivalentne kuju kasutades def
-blokki:
def gen(lst):
for i in lst:
if i%2 == 0:
if i >= 50:
yield i + 1
else:
yield i + 5
for i in gen(a):
print(i, end=' ')
27 51 99 49
Generaatoravaldiste kautamine funktsiooni argumendi sulgudes¶
Nii nagu eespool nägime saame generaatorit ja ka selle poolt loodud iteraatorit kasutada sisseehitatud funktsioonide argumendina. Täpsemalt öeldes vähemalt nendes funktsioonides mis oskavad üle iteraatortoega objektide itereerida. Ka generaatoravaldist saab kasutada funktsiooni argumendina $-$ sellisel juhul pole vaja kasutada kahte paari sulge.
Näide: Leiame esimese miljoni täisarvu ruutude summa kasutades sisseehitatud funktsiooni sum
.
sum(i**2 for i in range(1_000_000)) # Funktsiooni sum sulud defineerivad generaatori.
333332833333500000
Generaatorite järjestikku rakendamine (pipelining generators)¶
Python võimaldab generaatorite järjest rakendamist. Protsess sananeb liitfunktsioonide mõistele matemaatikas. Näiteks leiame Fibonacci jada liikmete ruutude ruutjuured kasutades generaatorite järjest rakendamist. Allolevas koodis genereeritakse (mitterekursiivselt) esimene Fibonacci jaga liige, siis rakendatakse sellele ruutu tõstmine ja lõpuks seda juuritakse. Peale esimese tulemuse väljastuse teostatakse sama protsess teise Fibonacci jada liikmega, jne.:
def fibonacci(n):
x, y = 0, 1
for _ in range(n): # Luuakse ainult n jada liiget.
x, y = y, x + y
yield x
def ruudud(xid):
for i in xid:
yield i**2
def juured(xid):
for i in xid:
yield int(i**0.5)
koos = juured(ruudud(fibonacci(10))) # Liititeraatori loomine
for i in koos: # Laisk itereerimine üle iteraatori.
print(i, end=' ')
1 1 2 3 5 8 13 21 34 55
Generaatoreid saab järjest rakendada ka funktsioonide argumendi sulgudes. Näiteks Fibonacci jada kümne esimese liikme ruutude summa on leitav nii:
sum(ruudud(fibonacci(10)))
4895
Mis vahe on iteraatortoega objektil (iterable) ja generaatori poolt loodud iteraatoril?¶
Iteraatortoega objektile saame rakendada funktsiooni iter
ehk teisendada seda iteraatoriks. Generaatorile pole võimalik rakendada funktsiooni iter
ja iteraatorile pole seda vast mõtet teha. Üle iteraatortoega objekti saab itereerida ainult for
- või while
-tsüklis, funktsioon next
neile ei rakendu aga iteraatoritele (ja generaatoritele) rakendub.
Allolevated näidetes vastavad maagilistele meetoditele või dunder-meetoditele __iter__
ja __next__
meile tuttavad sisseehitatud funktsioonid iter
ja next
.
a = [1, 2, 3, 4] # Iteraatortoega list.
dir(a) # Puudub meetod __next__ aga on meetod __iter__.
['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
iteraator = iter(a) # Teisendan listi iteraatoriks.
print(iteraator)
dir(iteraator) # Eksisteerib meetod __next__ ja __iter__.
<list_iterator object at 0x1449b3e50>
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__length_hint__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__']
Kuna funktsioonile iter
vastab dunder-meetod __iter__
saame funktsiooni iter
väljakutse asendada meetodi rakendamisega:
iteraator2 = a.__iter__() # Ekvivalentne tulemus eelmises rakus näidatuga.
print(iteraator2)
<list_iterator object at 0x1449b3fa0>
Iteraatorile saame rakendada sisseehitatud funktsiooni next
ja sellele vastavat dunder-meetodit __next__
. Iteraatortoega objektile seevastu ei saa:
print(next(iteraator2)) # Esimene iteraat.
print(iteraator2.__next__()) # Teine iteraat.
for i in iteraator2: # Laisk itereerimine, ülejäänud iteraadid.
print(' ', i)
1 2 3 4
next(a) # Kasutan eespool loodud listi a.
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[46], line 1 ----> 1 next(a) TypeError: 'list' object is not an iterator