Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

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.


 

Predchádzajúci článok
Kvíz - Tuples, množiny, iterátory a ďalšie kolekcie v Pythone
Všetky články v sekcii
Kolekcia v Pythone
Preskočiť článok
(neodporúčame)
Komprehencie, lambda výrazy a funkcie v Pythone
Článok pre vás napísal synek.o
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Aktivity