Vianoce v ITnetwork sú tu! Dobí si teraz kredity a získaj až 80 % extra kreditov na e-learningové kurzy ZADARMO. Zisti viac.
Hľadáme nové posily do ITnetwork tímu. Pozri sa na voľné pozície a pridaj sa k najagilnejšej firme na trhu - Viac informácií.

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.


 

Predchádzajúci článok
Iterátory v Pythone
Všetky články v sekcii
Kolekcia v Pythone
Preskočiť článok
(neodporúčame)
Regulárne výrazy v Pythone
Článok pre vás napísal synek.o
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Aktivity