IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
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í.

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 __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 = kolekce.__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.


 

Předchozí článek
Iterátory v Pythonu
Všechny články v sekci
Kolekce v Pythonu
Přeskočit článek
(nedoporučujeme)
Regulární výrazy v Pythonu
Článek pro vás napsal synek.o
Avatar
Uživatelské hodnocení:
193 hlasů
Aktivity