5. diel - Iterátory druhýkrát - Generátory v Pythone
V minulej lekcii, Iterátory v Pythone, sme si ukázali iteráciu, iterovateľné objekty a iterátory.
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
RandomNumberIterator
. Generovať bude vopred daný počet
náhodných čísel v zadanom intervale:
from random import randint class RandomNumberIterator: def __init__(self, number_count, lower_bound, upper_bound): self.number_count = number_count self.lower_bound = lower_bound self.upper_bound = upper_bound self.index = 1 def __iter__(self): return self def __next__(self): if self.index > self.number_count: raise StopIteration self.index +=1 return randint(self.lower_bound, self.upper_bound)
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 = RandomNumberIterator(5, 0, 100) print(iterator.__next__()) # or next(iterator) print(iterator.__next__()) for number in iterator: print(number, end=" ") print("\nThe iterator is now exhausted:") print([number for number in iterator])
Vo výstupe vidíme:
Konzolová aplikácia
86
23
9 3 15
The iterator is now exhausted:
[]
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
RandomNumberCollection
:
from random import randint class RandomNumberCollection: # Iterable object def __init__(self, number_count, lower_bound, upper_bound): self.number_count = number_count self.lower_bound = lower_bound self.upper_bound = upper_bound def __iter__(self): return RandomNumberIterator(self) class RandomNumberIterator: # Iterator def __init__(self, collection): self.collection = collection self.index = 1 def __next__(self): if self.index > self.collection.number_count: raise StopIteration self.index += 1 return randint(self.collection.lower_bound, self.collection.upper_bound)
Ak teraz vytvoríme inštanciu triedy RandomNumberCollection
,
môžeme na nej neobmedzene iterovať. O vytvorenie nového iterátora v rámci
každej iterácie sa trieda postará sama:
my_collection = RandomNumberCollection(5, 0, 100) for number in my_collection: print(number, end=" ") print() print([number for number in my_collection])
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 = my_collection.__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 generator_function(): yield "I" yield "am" yield "hungry" my_generator = generator_function()
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(my_generator)) print(next(my_generator)) print(next(my_generator))
Vo výstupe vidíme:
Konzolová aplikácia
I
am
hungry
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 RandomNumberCollection
a
namiesto klasického iterátora naimplementujeme
generátor:
from random import randint class RandomNumberCollection: def __init__(self, number_count, lower_bound, upper_bound): self.number_count = number_count self.lower_bound = lower_bound self.upper_bound = upper_bound def random_number_generator(self): # generator function for i in range(self.number_count): yield randint(self.lower_bound, self.upper_bound) def __iter__(self): return self.random_number_generator() # returns the generator collection = RandomNumberCollection(5, 0, 100) for number in collection: print(number, 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 RandomNumberCollection: def __init__(self, number_count, lower_bound, upper_bound): self.number_count = number_count self.lower_bound = lower_bound self.upper_bound = upper_bound def __iter__(self): return (randint(self.lower_bound, self.upper_bound) for _ in range(self.number_count))
Definícia triedy RandomNumberCollection
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
:
collection = RandomNumberCollection(5, 0, 100) for number in collection: print(number, 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 transactions.csv
vyzerá napríklad takto:
PID;Date;Type;Confirmed 12345;1.1.2023;DEPOSIT;YES 00000;1.1.2023;WITHDRAWAL;YES 99999;2.1.2023;WITHDRAWAL;NO
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("transactions.csv", "r") as file: transaction_list = file.readlines() for transaction in transaction_list[1:]: # skip the first line (header of the file) transaction = transaction.strip("\n") # remove the newline character pid, date, type_, confirmation = transaction.split(";") # unpack the list of parameters if confirmation == "NO": 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("transactions.csv") as file: next(file) # skip the first line (header of the file) for transaction in file: # iterate line by line transaction = transaction.strip("\n") # remove the newline character pid, date, type_, confirmation = transaction.split(";") # unpack the list of parameters if confirmation == "NO": 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 budúcej lekcii, Regulárne výrazy v Pythone , sa zameriame na regulárne výrazy.