Lekce 5 - Iterátory podruhé: Generátory v Pythonu
V minulé lekci, Iterátory v Pythonu, jsme si ukázali iteraci, iterovatelné objekty a iterátory.
V tomto tutoriálu kolekcí v Pythonu se ponoříme hlouběji do iterátorů. Vytvoříme si vlastní iterátor, seznámíme se s generátory a prozkoumáme jejich výhody.
Implementace vlastního iterátoru
Nejprve si vytvoříme vlastní iterátor a poté i iterovatelný objekt,
který bude využívat služeb tohoto iterátoru. Iterátor pojmenujeme
IteratorNahodnychCisel
. Generovat bude předem daný počet
náhodných čísel v zadaném intervalu:
from random import randint class IteratorNahodnychCisel: def __init__(self, pocet_cisel, spodni_hranice, horni_hranice): self.pocet_cisel = pocet_cisel self.spodni_hranice = spodni_hranice self.horni_hranice = horni_hranice self.index = 1 def __iter__(self): return self def __next__(self): if self.index > self.pocet_cisel: raise StopIteration self.index +=1 return randint(self.spodni_hranice, self.horni_hranice)
Pojďme si nyní vytvořit jeho instanci, která bude vracet pět náhodných
čísel v rozmezí 0 - 100
. Můžeme volat metodu
__next__()
a získat tak další prvek, nebo získat všechny prvky
najednou pomocí for
cyklu:
iterator = IteratorNahodnychCisel(5, 0, 100) print(iterator.__next__()) # nebo next(iterator) print(iterator.__next__()) for cislo in iterator: print(cislo, end=" ") print("\nZde již je iterátor vyčerpaný:") print([cislo for cislo in iterator])
Ve výstupu vidíme:
Konzolová aplikace
86
23
9 3 15
Zde již je iterátor vyčerpaný:
[]
Všimněme si, že for
cyklus nevypsal všech pět čísel, ale
pouze zbývající tři (dvě čísla jsme již vyčerpali dvojím zavoláním
metody __next__
). A pokud bychom opět zavolali metodu
__next__
, obdržíme výjimku StopIteration
.
Chceme-li na konci vytvořit seznam těchto prvků, výsledný seznam je prázdný. To proto, že iterátor je vyčerpán. Pokud chceme znovu iterovat, musíme vytvořit jeho novou instanci.
Nyní zvolíme sofistikovanější přístup. Do našeho kódu
zakomponujeme iterovatelný objekt v podobě třídy
KolekceNahodnychCisel
:
class KolekceNahodnychCisel: # Iterovatelný objekt def __init__(self, pocet_cisel, spodni_hranice, horni_hranice): self.pocet_cisel = pocet_cisel self.spodni_hranice = spodni_hranice self.horni_hranice = horni_hranice def __iter__(self): return IteratorNahodnychCisel(self) class IteratorNahodnychCisel: # Iterátor def __init__(self, kolekce): self.kolekce = kolekce self.index = 1 def __iter__(self): return self def __next__(self): if self.index > self.kolekce.pocet_cisel: raise StopIteration self.index += 1 return randint(self.kolekce.spodni_hranice, self.kolekce.horni_hranice)
Vytvoříme-li nyní instanci třídy KolekceNahodnychCisel
,
můžeme na ní neomezeně iterovat. O vytvoření nového iterátoru v rámci
každé iterace se třída postará sama:
maKolekce = KolekceNahodnychCisel(5, 0, 100) for cislo in maKolekce: print(cislo, end=" ") print() print([cislo for cislo in maKolekce])
Ve výstupu vidíme výsledek:
Konzolová aplikace
68 31 34 2 71
[63, 54, 24, 23, 8]
Potřebujeme-li pracovat přímo s iterátorem a volat metodu
__next__
, nic nám nebrání si příslušný iterátor vytvořit.
Jen musíme pamatovat na jeho omezení v podobě
vyčerpatelnosti:
iterator = maKolekce.__iter__() print(iterator.__next__()) print(next(iterator))
Ve výstupu vidíme:
Konzolová aplikace
32
61
Generátory
Generátory jsou speciální iterátory. Jejich velkou výhodou je daleko snazší a přímočařejší implementace. Vytvořit generátor lze pomocí generátorové funkce nebo generátorového výrazu.
Generátorová funkce
Generátorová funkce vrací objekt typu generator
. Je to
každá funkce, která obsahuje alespoň jeden příkaz yield
:
def generatorova_funkce(): yield "Já" yield "mám" yield "hlad" muj_generator = generatorova_funkce()
Samotný generátor vytvoříme tak, že tuto funkci zavoláme.
Na rozdíl od běžné funkce se kód umístěný v těle generátorové funkce neprovede, pouze se vrátí objekt generátoru.
Nyní máme k dispozici generátor, který se chová stejně jako nám již
dobře známý iterátor. Pokud budeme volat funkci next()
,
získáme jednotlivé prvky:
print(next(muj_generator)) print(next(muj_generator)) print(next(muj_generator))
Ve výstupu vidíme:
Konzolová aplikace
Já
mám
hlad
Po zavolání funkce next()
se vykonají příkazy v těle
generátorové funkce až po první yield
, tam se zastaví a
příslušná hodnota se vrátí. Při dalším zavolání se pokračuje od
tohoto místa, dokud nenarazí na další yield
atd. Jakmile
narazí na příkaz return
, anebo již nejsou další příkazy,
ukončí se.
Nyní se vrátíme k naší třídě KolekceNahodnychCisel
a
místo klasického iterátoru naimplementujeme generátor:
class KolekceNahodnychCisel: def __init__(self, pocet_cisel, spodni_hranice, horni_hranice): self.pocet_cisel = pocet_cisel self.spodni_hranice = spodni_hranice self.horni_hranice = horni_hranice def generator_nahodnych_cisel(self): # generátorová funkce for i in range(self.pocet_cisel): yield randint(self.spodni_hranice, self.horni_hranice) def __iter__(self): return self.generator_nahodnych_cisel() # vrací generátor kolekce = KolekceNahodnychCisel(5, 0, 100) for cislo in kolekce: print(cislo, end=" ")
Ve výstupu vidíme vygenerovaná náhodná čísla:
Konzolová aplikace
27 45 33 8 68
Generátorové výrazy
Jednoduché generátory mohou být vytvořeny elegantním způsobem v podobě generátorového výrazu. Syntaxe je téměř totožná jako při tvorbě seznamové komprehence. Rozdíl spočívá jen v tom, že místo hranatých závorek se použijí kulaté.
Použitím generátorového výrazu v našem ukázkovém programu se nám kód ještě lehce zredukuje a zvýší se jeho čitelnost:
class KolekceNahodnychCisel: def __init__(self, pocet_cisel, spodni_hranice, horni_hranice): self.pocet_cisel = pocet_cisel self.spodni_hranice = spodni_hranice self.horni_hranice = horni_hranice def __iter__(self): return (randint(self.spodni_hranice, self.horni_hranice) for _ in range(self.pocet_cisel))
Definice třídy KolekceNahodnychCisel
se zkrátila oproti
původní verzi na polovinu, aniž by postrádala jakoukoli funkcionalitu. To je
síla generátorů a klíčového slova yield
.
kolekce = KolekceNahodnychCisel(5, 0, 100) for cislo in kolekce: print(cislo, end=" ")
Ve výstupu vidíme vygenerovaná náhodná čísla:
Konzolová aplikace
68 31 34 2 71
Využití iterátorů v praxi
Slabou stránkou iterátorů je jejich jednorázové použití. Iterátory nicméně nabízejí velmi efektivní práci se zdroji. Pomocí iterátoru lze realizovat tzv. odložené vyhodnocování (lazy evaluation). Díky této strategii lze vyhodnotit určitý výraz až v okamžiku, kdy je jeho hodnota skutečně potřeba.
Představme si, že potřebujeme zpracovat nějaký velký soubor a získat z
něj určité informace. Příkladem by mohl být bankovní informační
systém, který by vygeneroval seznam transakcí za určité období ve formátu
csv
, kde by každý řádek reprezentoval jednu transakci. Obsah
začátku takového souboru transakce.csv
vypadá například
takto:
PID;Datum;Typ;Potvrzeno 12345;1.1.2023;VKLAD;ANO 00000;1.1.2023;VYBER;ANO 99999;2.1.2023;VYBER;NE
Pokud bychom potřebovali získat identifikátory nepotvrzených transakcí,
nejjednodušším řešením by bylo načíst obsah celého souboru do seznamu
pomocí metody readlines()
a následně ho zpracovat:
with open("transakce.csv", "r") as soubor: seznam_transakci = soubor.readlines() for transakce in seznam_transakci[1:]: # přeskočíme první řádek (hlavičku souboru) transakce = transakce.strip("\n") # odstraníme znak nového řádku pid, datum, typ, potvrzeni = transakce.split(";") # rozbalíme seznam parametrů if potvrzeni == "NE": print(pid)
Nicméně zdrojový soubor by v závislosti na zvoleném časovém období
mohl dosahovat i extrémních velikostí. Výše zvoleným způsobem bychom
celý soubor umístili do paměti počítače. My ale nepotřebujeme pracovat s
kompletním obsahem souboru naráz. Jelikož funkce open()
vrací
objekt IOWrapper
, což je iterátor, následující postup je
vhodnější:
with open("transakce.csv") as soubor: next(soubor) # přeskočíme první řádek (hlavičku souboru) for transakce in soubor: # iterujeme řádek po řádku transakce = transakce.strip("\n") # odstraníme znak nového řádku pid, datum, typ, potvrzeni = transakce.split(";") # rozbalíme seznam parametrů if potvrzeni == "NE": print(pid)
Nyní načítáme obsah souboru postupně řádek po řádku, tudíž v jeden okamžik využíváme pouze tolik paměti počítače, kolik zabírají data jedné transakce. Výhodou tohoto přístupu je možnost zpracování souboru libovolné velikosti.
V příští lekci, Regulární výrazy v Pythonu, se zaměříme na regulární výrazy.