6. diel - Iterátory druhýkrát: Generátory
V predchádzajúcom kvíze, Kvíz - Tuples, množiny, iterátory a ďalšie kolekcie v Pythone, sme si overili nadobudnuté skúsenosti z predchádzajúcich lekcií.
V tomto tutoriále kolekcií v Pythone sa ponoríme hlbšie do iterátorov. Vytvoríme si vlastný iterátor, zoznámime sa s generátormi a preskúmame ich výhody.
Implementácia vlastného iterátora
Najprv si vytvoríme vlastný iterátor a potom aj iterovateľný objekt,
ktorý bude využívať služby tohto iterátora. Iterátor pomenujeme
IteratorNahodnychCisel
. Generovať bude vopred daný počet
náhodných čísel v zadanom intervale:
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)
Poďme si teraz vytvoriť jeho inštanciu, ktorá bude vracať päť
náhodných čísel v rozmedzí 0 - 100
. Môžeme volať metódu
__next__()
a získať tak ďalší prvok, alebo získať všetky
prvky naraz pomocou 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])
Vo výstupe vidíme:
Konzolová aplikácia
86
23
9 3 15
Zde již je iterátor vyčerpaný:
[]
Všimnime si, že for
cyklus nevypísal všetkých päť
čísel, ale iba zvyšné tri (dve čísla sme už vyčerpali dvojitým
zavolaním metódy __next__
). A ak by sme opäť zavolali metódu
__next__
, dostaneme výnimku StopIteration
.
Ak chceme na konci vytvoriť zoznam týchto prvkov, výsledný zoznam je prázdny. To preto, že iterátor je vyčerpaný. Ak chceme znova iterovať, musíme vytvoriť jeho novú inštanciu.
Teraz zvolíme sofistikovanejší prístup. Do nášho kódu
zakomponujeme iterovateľný objekt v podobe triedy
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 __next__(self): if self.index > self.kolekce.pocet_cisel: raise StopIteration self.index += 1 return randint(self.kolekce.spodni_hranice, self.kolekce.horni_hranice)
Ak teraz vytvoríme inštanciu triedy KolekceNahodnychCisel
,
môžeme na nej neobmedzene iterovať. O vytvorenie nového iterátora v rámci
každej iterácie sa trieda postará sama:
maKolekce = KolekceNahodnychCisel(5, 0, 100) for cislo in maKolekce: print(cislo, end=" ") print() print([cislo for cislo in maKolekce])
Vo výstupe vidíme výsledok:
Konzolová aplikácia
68 31 34 2 71
[63, 54, 24, 23, 8]
Ak potrebujeme pracovať priamo s iterátorom a volať metódu
__next__
, nič nám nebráni si príslušný iterátor vytvoriť.
Len musíme pamätať na jeho obmedzenie v podobe
vyčerpateľnosti:
iterator = kolekce.__iter__() print(iterator.__next__()) print(next(iterator))
Vo výstupe vidíme:
Konzolová aplikácia
32
61
Generátory
Generátory sú špeciálne iterátory. Ich veľkou výhodou je ďaleko jednoduchšia a priamočiarejšia implementácia. Vytvoriť generátor je možné pomocou generátorovej funkcie alebo generátorového výrazu.
Generátorová funkcia
Generátorová funkcia vracia objekt typu generator
. Je to
každá funkcia, ktorá obsahuje aspoň jeden príkaz yield
:
def generatorova_funkce(): yield "Já" yield "mám" yield "hlad" muj_generator = generatorova_funkce()
Samotný generátor vytvoríme tak, že túto funkciu zavoláme.
Na rozdiel od bežnej funkcie sa kód umiestnený v tele generátorovej funkcie nevykoná, iba sa vráti objekt generátora.
Teraz máme k dispozícii generátor, ktorý sa správa rovnako ako nám už
dobre známy iterátor. Pokiaľ budeme volať funkciu next()
,
získame jednotlivé prvky:
print(next(muj_generator)) print(next(muj_generator)) print(next(muj_generator))
Vo výstupe vidíme:
Konzolová aplikácia
Já
mám
hlad
Po zavolaní funkcie next()
sa vykonajú príkazy v tele
generátorovej funkcie až po prvé yield
, tam sa zastavia a
príslušná hodnota sa vráti. Pri ďalšom zavolaní sa pokračuje od tohto
miesta, kým nenarazí na ďalšie yield
atď. Akonáhle narazí na
príkaz return
, alebo už nie sú ďalšie príkazy, ukončí
sa.
Teraz sa vrátime k našej triede KolekceNahodnychCisel
a
namiesto klasického iterátora 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=" ")
Vo výstupe vidíme vygenerované náhodné čísla:
Konzolová aplikácia
27 45 33 8 68
Generátorové výrazy
Jednoduché generátory môžu byť vytvorené elegantným spôsobom v podobe generátorového výrazu. Syntax je takmer totožná ako pri tvorbe zoznamovej komprehencie. Rozdiel spočíva len v tom, že namiesto hranatých zátvoriek sa použijú okrúhle.
Použitím generátorového výrazu v našom ukážkovom programe sa nám kód ešte ľahko zredukuje a zvýši sa jeho čitateľnosť:
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))
Definícia triedy KolekceNahodnychCisel
sa skrátila oproti
pôvodnej verzii na polovicu bez toho, aby postrádala akúkoľvek
funkcionalitu. To je sila generátorov a kľúčového slova
yield
.
kolekce = KolekceNahodnychCisel(5, 0, 100) for cislo in kolekce: print(cislo, end=" ")
Vo výstupe vidíme vygenerované náhodné čísla:
Konzolová aplikácia
68 31 34 2 71
Využitie iterátorov v praxi
Slabou stránkou iterátorov je ich jednorazové použitie. Iterátory však ponúkajú veľmi efektívnu prácu so zdrojmi. Pomocou iterátora je možné realizovať tzv. odložené vyhodnocovanie (lazy evaluation). Vďaka tejto stratégii je možné vyhodnotiť určitý výraz až v okamihu, keď je jeho hodnota skutočne potrebná.
Predstavme si, že potrebujeme spracovať nejaký veľký súbor a získať z
neho určité informácie. Príkladom by mohol byť bankový informačný
systém, ktorý by vygeneroval zoznam transakcií za určité obdobie vo
formáte csv
, kde by každý riadok reprezentoval jednu transakciu.
Obsah začiatku takého súboru transakce.csv
vyzerá napríklad
takto:
PID;Datum;Typ;Potvrzeno 12345;1.1.2023;VKLAD;ANO 00000;1.1.2023;VYBER;ANO 99999;2.1.2023;VYBER;NE
Ak by sme potrebovali získať identifikátory nepotvrdených transakcií,
najjednoduchším riešením by bolo načítať obsah celého súboru do zoznamu
pomocou metódy readlines()
a následne ho spracovať:
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)
Avšak zdrojový súbor by v závislosti od zvoleného časového obdobia
mohol dosahovať aj extrémne veľkosti. Vyššie zvoleným spôsobom by sme
celý súbor umiestnili do pamäte počítača. My ale nepotrebujeme pracovať s
kompletným obsahom súboru naraz. Keďže funkcia open()
vracia
objekt IOWrapper
, čo je iterátor, nasledujúci postup je
vhodnejší:
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)
Teraz načítame obsah súboru postupne riadok po riadku, teda v jeden okamih využívame iba toľko pamäte počítača, koľko zaberajú dáta jednej transakcie. Výhodou tohto prístupu je možnosť spracovania súboru ľubovoľnej veľkosti.
V ďalšej lekcii, Komprehencie, lambda výrazy a funkcie v Pythone , si ukážeme zoznamové komprehencie, lambda výrazy a niektoré vstavané funkcie pre prácu s iterovateľnými objektmi.