4. Iterátory, generátory, binárne súbory


Iterátory

Už vieme, že v Pythone sú niektoré základné typy iterovateľné - môžeme prechádzať ich prvky, napríklad pomocou for cyklu, Stretli sme sa s týmito iterovateľnými typmi:

  • list, tuple, str, dict, set

  • postupnosť celých čísel range(...), otvorený súbor na čítanie open(...)

  • výsledky funkcií map() a filter() aj generátorová notácia [... for ...]

Aj pre vlastný definovaný typ môžeme zabezpečiť iterovateľnosť. Možností je niekoľko, ukážeme dve z nich:

  1. v triede zadefinujeme dvojicu magických metód __iter__() a __next__(), ktoré zabezpečia iterovateľnosť

  2. v triede zadefinujeme magickú metódu __getitem__(): v prípade prechádzania pomocou for-cyklu, Python zabezpečí postupné generovanie indexov od 0 vyššie a pri prvom neexistujúcom prvku, skončí

Pozrime sa najprv na 1. spôsob vytvorenia iterovateľnosti a to pomocou štandardných funkcií iter() a next() (pomocou metód __iter__() a __next__() vieme zabezpečiť funkčnosť aj pre našu novú triedu). Aby sme lepšie pochopili ich princíp fungovania, vysvetlime, ako Python „vidí“ obyčajný for-cyklus. Python ho vnútorne realizuje pomocou while-cyklu a iterátora. Napríklad takýto for-cyklus:

zoznam = [2, 3, 5, 7, 11, 13, 17]
for i in zoznam:
    print(i, i*i)        # telo cyklu

v skutočnosti Python realizuje pomocou iterátora približne takto:

iterator = iter(zoznam)
while True:
    try:
        i = next(iterator)
        print(i, i*i)        # telo cyklu
    except StopIteration:
        break

Vysvetlime si to:

  1. Štandardná funkcia iter získa (si vypýta) od objektu iterátor, ktorý bude umožňovať postupné získavanie prvkov objektu. Táto funkcia (zrejme je polymorfná) najčastejšie funguje tak, že samotný objekt má definovanú magickú metódu __iter__ a funkcia iter(objekt) potom vráti výsledok volania objekt.__iter__(). Táto magická funkcia musí vrátiť inštanciu takej triedy, ktorá obsahuje metódu __next__, tzv. iterátorový objekt.

  2. Štandardná funkcia next dostava ako parameter iterátor. Keďže iterátor má definovanú metódu __next__, tak volanie next(iterator) v skutočnosti zavolá iterator.__next__(), Volanie tejto magickej metódy vráti další iterovaný prvok.

  3. Keď __next__ namiesto iterovaného prvku vráti výnimku StopIteration, označuje to, že iterovanie skončilo a teda skončil aj for-cyklus.

Môžete to vidieť aj na tomto príklade s iterovaním znakového reťazca:

>>> it = iter('ahoj')
>>> next(it)
    'a'
>>> next(it)
    'h'
>>> next(it)
    'o'
>>> next(it)
    'j'
>>> next(it)
    ...
    StopIteration

Zrejme trieda str pre znakové reťazce obsahuje magickú metódu __iter__, ktorá vráti nejaký pripravený iterátor a tento má k dispozícii magickú metódu __next__, ktorá postupne vracia prvky iterovaného objektu (reťazca 'ahoj').

Prvý príklad definuje triedu, ktorá obsahuje obe magické metódy __iter__ aj __next__. V tomto prípade volanie __iter__ vráti samotný objekt:

class Test:
    def __init__(self, od, do):
        self. od, self.do = od, do

    def __iter__(self):
        self.x = self.od
        return self

    def __next__(self):
        if self.x > self.do:
            raise StopIteration
        self.x += 1
        return self.x - 1

Otestujeme:

>>> test = Test(3, 6)
>>> test
    <__main__.Test object at 0x000002B958A51A00>
>>> it = iter(test)
>>> it
    <__main__.Test object at 0x000002B958A51A00>

Vidíme, že inštancia test je identická s iterátorom. Môžeme povedať, že inštancia test je sama sebe iterátorom. Zrejme, volanie iter(test) je v tomto prípade volaním test.__iter__(), ktoré nastaví atribút x a vráti samého seba, teda referenciu test.

Keď už máme iterátor, môžeme z neho pomocou funkcie next postupne získavať všetky prvky:

>>> next(it)
    3
>>> next(it)
    4
>>> next(it)
    5
>>> next(it)
    6
>>> next(it)
    ...
    StopIteration

Môžeme otestovať aj for-cyklus:

t = Test(2, 5)
for i in t:
    print(i)

vypíše:

2
3
4
5

Problém by mohli robiť vnorené cykly, keďže tento náš iterátor má jedinú premennú ix pre oba cykly:

t = Test(2, 5)
for i in t:
    for j in t:
        print(i, j)

vypíše:

2 2
2 3
2 4
2 5

Hoci by sme v tomto prípade očakávali 16 riadkov dvojíc čísel.

Takže si treba uvedomiť, že ak nám stačí iterátor, ktorý sa nebude používať vo vnorených for-cyklosch, môžeme obe metódy __iter__ aj __next__ zadefinovať v samotnej triede. Ak ale chceme korektne fungujúci iterátor, ktorý bude iterovať správne hodnoty aj vo vnorenom cykle, musíme to organizovať inak.

Teraz zadefinujeme iterátor ako inštanciu inej triedy (napríklad TestIterator) a metóda __iter__ vráti referenciu na takýto iterátorový objekt. Táto iterátorová trieda bude obsahovať metódu __next__, úplne rovnakú, ako v predchádzajúcom riešení. Teraz volanie __iter__ vždy vráti nový iterátor s vlastným počítadlom cyklu x:

class Test:
    def __init__(self, od, do):
        self. od, self.do = od, do

    def __iter__(self):
        return TestIterator(self. od, self.do)

class TestIterator:
    def __init__(self, od, do):
        self.x, self.do = od, do

    def __next__(self):
        if self.x > self.do:
            raise StopIteration
        self.x += 1
        return self.x - 1

Otestujte, či vnorené for-cykly teraz funguju korektne.


Iterátor pomocou __getitem__()

V predchádzajúcej prednáške sme videli príklad, v ktorom sa vďaka metóde __getitem__() stala nejaká trieda (spájaný zoznam) iterovateľnou. Ak nemáme v triede definované metódy __iter__() a __next__(), ale nachádza sa v nej __getitem__(), Python z vlastnej iniciatívy „pochopí“, že takáto štruktúra by sa mohla dať prechádzať aj for-cyklom (iterovať). Veď zrejme mu stačí postupne indexovať s indexom 0, potom 1, potom 2, atď. až kým to nespadne na chybe a vtedy ukončí aj for-cyklus (bez chybovej správy).

Zadefinujme vlastnú triedu s metódou __getitem__():

class Test1:
    def __getitem__(self, ix):
        if ix < 4:
            return (ix + 1) ** 2
        raise IndexError

Otestujme:

>>> a = Test1()
>>> for i in range(5):
        print(i, a[i])

    0 1
    1 4
    2 9
    3 16
    ...
    IndexError

Vďaka __getitem__ vieme indexovať prvky inštancie, ale len od 0 do 3.

My už ale vieme, že takáto inštancia je iterovateľná, teda:

>>> it = iter(a)
>>> it
    <iterator object at 0x000001D1F28A1E40>
>>> next(it)
    1
>>> next(it)
    4
>>> next(it)
    9
>>> next(it)
    16
>>> next(it)
    ...
    StopIteration

Napriek tomu, že sme definovali len magickú metódu __getitem__, Python tu z vlastnej iniciatívy dodefinoval __iter__ s nejakým iterátorom, ktorý definuje správny __next__. Teraz funguje nielen obyčajný for-cyklus:

>>> for i in a:
        print(i)

    1
    4
    9
    16

Ale fungujú aj vnorené cykly:

>>> for i in a:
        for j in a:
            print((i, j), end=' ')
        print()

    (1, 1) (1, 4) (1, 9) (1, 16)
    (4, 1) (4, 4) (4, 9) (4, 16)
    (9, 1) (9, 4) (9, 9) (9, 16)
    (16, 1) (16, 4) (16, 9) (16, 16)

Zhrňme: iterátor je taký objekt, pomocou ktorého môžeme postupne prechádzať prvky nejakej „kolekcie“. Zrejme sa táto kolekcia bude skladať z nejakých prvkov. Hovoríme, že objekt je iterovateľný (iterable), keď sa dajú prechádzať jeho prvky buď pomocou iter() a next() alebo indexovaním __getitem__.


Generátory


V Pythone existuje zaujímavý spôsob ako generovať postupnosti. Najčastejšie sme to doteraz robili pomocou zoznamu, napríklad generovanie všetkých deliteľov nejakého čísla:

def delitele(n):
    res = []
    for i in range(1, n + 1):
        if n % i == 0:
            res.append(i)
    return res
>>> delitele(100)
    [1, 2, 4, 5, 10, 20, 25, 50, 100]

Pre postupnosti je základnou vlastnosťou to, aby boli iterovateľné, t. j. aby sa jeho prvky dali postupne prechádzať pomocou for-cyklu. Napríklad:

>>> for i in delitele(100):
        print(i, end=' ')
    1 2 4 5 10 20 25 50 100

Teda nie je dôležité mať k dispozícii naraz všetky prvky v nejakej dátovej štruktúre, ale je dôležité ich postupne získavať až vždy, keď ich budeme potrebovať (teda vlastne niečo ako __iter__() a __next__()). Na toto využijeme nový mechanizmus generátorov - bude to ďalší spôsob vytvárania iterátora. Tieto sa podobajú na bežné funkcie, ale namiesto return používajú príkaz yield. Generátory fungujú na takomto princípe:

  • keď takúto generátorovú funkciu zavoláme, nevytvorí sa ešte žiadna hodnota, ale vytvorí sa generátorový objekt (pri iterátoroch sme tomu hovorili iterátorový objekt)

  • keď si od generátorového objektu teraz vypýtame jednu hodnotu, dozvieme sa prvú z nich (slúži na to štandardná funkcia next())

  • každé ďalšie vypýtanie hodnoty (funkcia next()) nám odovzdá ďalšiu hodnotu postupnosti

  • keď už generátorový objekt nemá ďalšiu hodnotu, tak volanie funkcie next() vyvolá chybovú správu StopIteration

Samotná generátorová funkcia pri výskyte príkazu yield nekončí „len“ odovzdá jednu z hodnôt postupnosti a pokračuje ďalej. Funkcia končí až na príkaze return (alebo na konci funkcie) a vtedy automaticky vygeneruje chybu (exception) StopIteration. Samotné odovzdanie hodnoty (príkazom yield) preruší vykonávanie generátorovej funkcie s tým, že sa presne zapamätá miesto, kde sa bude pokračovať aj s momentálnym menným priestorom. Volanie next() pokračuje na tomto mieste, aby odovzdal ďalšiu hodnotu.

Zadefinujme funkciu delitele() ako generátor:

def delitele(n):
    for i in range(1, n+1):
        if n % i == 0:
            yield i

vytvoríme generátorový objekt:

>>> d = delitele(15)

premenná d je naozaj generátorový objekt, ktorý zatiaľ nevygeneroval žiadny prvok postupnosti:

>>> d
    <generator object delitele at 0x0000000003042828>

keď chceme prvý prvok, zavoláme metódu next() rovnako ako pri iterátoroch:

>>> next(d)
    1

každé ďalšie volanie next() vygeneruje ďalšie prvky:

>>> next(d)
    3
>>> next(d)
    5
>>> next(d)
    15
>>> next(d)
    ...
    StopIteration

Po poslednom prvku funkcia next() vyvolala výnimku StopIteration. Mohli by sme to zapísať aj pomocou for-cyklu:

>>> for i in delitele(15):
        print(i, end=' ')
    1 3 5 15

Ukážky generátorových funkcií

  • postupnosť piatich hodnôt:

    def prvo():
        yield 2
        yield 3
        yield 5
        yield 7
        yield 11
    
    >>> list(prvo())
        [2, 3, 5, 7, 11]
    
  • to isté pomocou for-cyklu:

    def prvo():
        for i in [2, 3, 5, 7, 11]:
            yield i
    
    >>> list(prvo())
        [2, 3, 5, 7, 11]
    

For-cyklus v generátorových funkciách, ktorý generuje yield, môžeme skrátene zapísať aj pomocou verzie yield from:

  • to isté ako predchádzajúca verzia:

    def prvo():
        yield from [2, 3, 5, 7, 11]
    
    >>> list(prvo())
        [2, 3, 5, 7, 11]
    

Parametrom yield from môže byť ľubovoľný iterovateľný objekt nielen zoznam, napríklad aj range() alebo aj iný generátorový objekt (napríklad v rekurzívnych funkciách).

  • využitie range():

    def test(n):
         yield from range(n+1)
         yield from range(n-1, -1, -1)       # alebo reversed(range(n))
    
    >>> list(test(3))
        [0, 1, 2, 3, 2, 1, 0]
    >>> list(test(5))
        [0, 1, 2, 3, 4, 5, 4, 3, 2, 1, 0]
    
  • skoro to isté ale rekurzívne:

    def urob(n):
        if n < 1:
            yield 0
        else:
            yield n
            yield from urob(n-1)
            yield n
    
    >>> list(urob(3))
        [3, 2, 1, 0, 1, 2, 3]
    >>> list(urob(5))
        [5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5]
    
  • fibonacciho postupnosť:

    def fib(n):
        a, b = -1, 1
        while n > 0:
            a, b = b, a+b
            yield b
            n -= 1
    
    >>> list(fib(10))
        [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
    >>> for i in fib(10):
            print(i, end=' ')
        0 1 1 2 3 5 8 13 21 34
    

    ak by sme chceli z fibonacciho postupnosti vypísať len po prvý člen, ktorý je aspoň 10000 a my nevieme odhadnúť, koľko ich budeme potrebovať, zapíšeme napríklad:

    >>> for i in fib(10000):
            print(i, end=' ')
            if i > 10000:
                break
        0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946
    

vďaka tomu, že fib() je generátor a nie funkcia, ktorá vytvára zoznam hodnôt, nebolo pre tento for-cyklus potrebné vyrobiť 10000 prvkov, ale len toľko, koľko ich bolo treba v cykle.

Generátorovým funkciám se niekedy hovorí lenivé vyhodnocovanie (lazy evaluation), lebo funkcia počíta ďalšiu hodnotu až keď je o ňu požiadaná (pomocou next()) - teda nič nepočíta zbytočne dopredu.

Generované zoznamy

už sme sa dávnejšie stretli so zápismi:

>>> zoznam = [i for i in range(20) if i%7 in [2,3,5]]
>>> zoznam
    [2, 3, 5, 9, 10, 12, 16, 17, 19]
>>> mn = {i for i in range(20) if i%7 in [2,3,5]}
>>> mn
    {2, 3, 5, 9, 10, 12, 16, 17, 19}
>>> ntica = tuple(i for i in range(20) if i%7 in [2,3,5])
>>> ntica
    (2, 3, 5, 9, 10, 12, 16, 17, 19)

Podobne vieme vygenerovať nielen zoznam (list), množinu (set) a n-ticu (tuple), ale aj slovník (dict). Hovoríme tomu list comprehension (resp. iný typ) - po slovensky generované zoznamy (niekedy aj generátorový zápis alebo notácia). Všimnite si, že n-ticu musíme generovať pomocou funkcie tuple(), lebo inak:

>>> urob = (i for i in range(20) if i%7 in [2,3,5])
>>> urob
    <generator object <genexpr> at 0x022A6760>
>>> list(urob)
    [2, 3, 5, 9, 10, 12, 16, 17, 19]

dostávame generátorový objekt úplne rovnaký ako napríklad:

def gg():
    for i in range(20):
        if i%7 in [2,3,5]:
            yield i
>>> urob = gg()
>>> urob
    <generator object gg at 0x022A6828>
>>> list(urob)
    [2, 3, 5, 9, 10, 12, 16, 17, 19]

Takže jednoduché generátorové objekty môžeme vytvárať aj takto zjednodušene:

def gg(*zoznam):
    return (i for i in range(20) if i%7 in zoznam)
>>> urob = gg(2, 3, 5)
>>> urob
    <generator object <genexpr> at 0x0229E8A0>
>>> list(urob)
    [2, 3, 5, 9, 10, 12, 16, 17, 19]

Zhrnutie: generátor je funkcia, ktorá postupne generuje prvky nejakej kolekcie. Generátor je vlastne ďalším spôsobom, ako vytvoriť iterátor (každý generátor je iterátor, ale nie každý iterátor je generátor), teda aj generátor je iterovateľný.


Binárne súbory


V minulom semestri sme sa zoznámili s prácou s textovými súbormi:

  • textový súbor môžeme chápať ako postupnosť riadkov, pričom každý riadok je znakový reťazec ukončený znakom '\n'

  • takýto súbor sa najčastejšie nachádza na nejakom externom médiu, napríklad na disku alebo USB (najčastejšie majú príponu '.txt', '.py', '.html', '.js', '.css', '.xml', '.svg', …) a môžeme ich pozrieť alebo upravovať v rôznych textových editoroch, napríklad aj v pythonovskom IDLE

  • aby sme mohli pracovať so súborom, musíme ho najprv otvoriť (jedna z možností):

    • na čítanie - ďalej sa umožní čítať riadok za riadkom (alebo postupnsoť znakov)

    • na zápis - vymaže doterajší obsah a umožní postupne zapisovať riadok za riadkom (alebo postupnsoť znakov)

    • na zápis s ponechaním pôvodného obsahu

  • po ukončení práce so súborom by sme ho mali zatvoriť,aby sme ho neblokovali pre operačný systém

Nie všetky súbory v súborových systémoch sú ale textové. Sú to napríklad rôzne grafické súbory, napríklad '.png', '.bmp', '.jpg', … ale aj komprimované súbory ako '.zip', '.rar', '.cab', …, vykonateľné súbory ako '.exe', '.com', '.dll', … alebo rôzne dátové súbory so známou alebo s neznámou štruktúrou '.doc', '.odt', '.pdf', '.dat', …

Takéto súbory, ak otvoríme v textovom editore, sa zobrazia s množstvom divných znakov. Ak by sme ich ale zobrazili v editoroch, ktoré zvládajú šestnástkový výpis, videli by sme niečo takéto:

../_images/l04_01.png

Zobrazuje sa postupnosť bajtov, pričom niektoré z nich zodpovedajú niektorým ASCII znakom.


Typ bytes

Binárny súbor je postupnosť bajtov. V Pythone budeme zapisovať do binárneho súboru a čítať z neho nie znakové reťazce, ale nový typ bytes. Tento typ je nemeniteľnou (immutable) postupnosťou bajtov, teda celých čísel z intervalu <0, 255>. V Pythone zapisujeme takéto postupnosti v tvare znakového reťazca, pred ktorým je prilepený znak b:

>>> b'+ A'                         # trojbajtová postupnosť
    b'+ A'
>>> list(b'+ A')                   # tento zápis reprezentuje ASCII-hodnoty týchto znakov
    [43, 32, 65]
>>> bytes((45, 49, 70, 100))       # postupnosť bajtov vieme skonštruovať aj zo zoznamu čísel
    b'-1Fd'
>>> bytes()                        # prázdna postupnosť bajtov
    b''
>>> bytes('Python', 'utf8')        # postupnosť bajtov vieme skonštruovať aj zo znakového reťazca
    b'Python'                      # vtedy musíme uviesť aj kódovanie
>>> tuple(b'Python')
    (80, 121, 116, 104, 111, 110)

Takto vidíme zobrazené bajty, ktoré majú v ASCII svoju reprezentáciu. Ak potrebujeme zadať bajt, ktorý nemá v ASCII svoje zobrazenie, použije sa zadávanie kódu bajtu v šestnástkovej sústave:

>>> b'A\x05B'                      # trojbajtová postupnosť
    b'A\x05B'
>>> tuple(b'A\x05B')               # '\x05' označuje jeden bajt s hodnotou 5
    (65, 5, 66)
>>> b = bytes(range(130, 250, 10)) # dvanásť bajtová postupnosť čísel
>>> b
    b'\x82\x8c\x96\xa0\xaa\xb4\xbe\xc8\xd2\xdc\xe6\xf0'  # čísla sú v šestnástkovej sústave
>>> tuple(b)
    (130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240)
>>> for i in b:                    # postupnosť vieme prechádzať for-cyklom
        print(i, end=' ')
    130 140 150 160 170 180 190 200 210 220 230 240

Okrem nemeniteľného (immutable) štandardného typy bytes v Pythone môžete využiť meniteľný typ bytearray, ktorý funguje skoro presne rovnako. Viac informácií bytearray.


Práca s binárnym súborom

S binárnym súborom pracujeme veľmi podobne ako s textovým. Zápis do súboru:

with open('subor.dat', 'wb') as subor:     # 'wb' označuje zápis do binárneho súboru
    subor.write(b'student')
    subor.write(bytes(range(10)))

Zapíše 17 bajtov, najprv 7 bajtov ASCII kodov reťazca 'student' a za tým 10 bajtov s hodnotami 0, 1, 2, …, 9.

Čítanie zo súboru:

with open('subor.dat', 'rb') as subor:     # 'rb' označuje čítanie z binárneho súboru
    prvy = subor.read(7)
    bajt = subor.read(1)
    druhy = b''
    while bajt != b'':                     # kým nie je koniec súboru
        druhy += bajt
        bajt = subor.read(1)

Tento program najprv prečíta prvých 7 bajtov do premennej prvy (zrejme bude obsahovať postupnosť b'student') a do premennej druhy prečíta zvyšné bajty súboru (zrejme b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09', čo sa môže zobraziť ako b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t'). Dalo by sa to zapísať aj takto jednoducho:

with open('subor.dat', 'rb') as subor:
    prvy = subor.read(7)               # prvých 7 bajtov
    druhy = subor.read()               # celý zvyšok súboru až do konca

Konverzie dát z a do postupnosti bajtov

Na stránke Bytes Objects v Pythonovom helpe si môžete prečítať ďalšie možnosti práce s týmto typom (napríklad, užitočnú metódu decode), ale užitočné môžu byť aj niektoré metódy s celočíselným typom (napríklad, to_bytes a from_bytes). Preštudujte si nasledovné ukážky:

  • ako prerobiť postupnosť bajtov (typ bytes) na obyčajný znakový reťazec (typ str):

    >>> bajty = b'Kovac Igor'
    >>> ''.join(chr(i) for i in bajty)   # pomocou chr pre každý bajt a potom join
        'Kovac Igor'
    >>> ''.join(map(chr, bajty))         # pomocou mapovania každého bajtu na znak a join
        'Kovac Igor'
    >>> bajty.decode()                   # pomocou metódy decode
        'Kovac Igor'
    
  • ako z celého čísla vyrobiť postupnosť štyroch bajtov (na začiatku je najnižší bajt):

    >>> cislo = 1234567
    >>> bajty = b''
    >>> for i in range(4):               # pomocou for-cyklu a postupným delením 256
            bajty += bytes((cislo%256,))
            cislo //= 256
    >>> bajty
        b'\x87\xd6\x12\x00'
    

    alebo pomocou metódy:

    >>> cislo = 1234567
    >>> cislo.to_bytes(4, 'little')   # parameter 'little' označuje, že začína najnižším bajtom
        b'\x87\xd6\x12\x00'
    
  • ako z postupnosti štyroch bajtov vyrobiť celé číslo:

    >>> bajty = b'\xb1\x7f\x39\x05'
    >>> cislo = 0
    >>> for bajt in reversed(bajty):
              cislo = cislo*256 + bajt
    >>> cislo
        87654321
    

    alebo pomocou metódy:

    >>> bajty = b'\xb1\x7f\x39\x05'
    >>> int.from_bytes(bajty, 'little') # parameter 'little' označuje, že začína najnižším bajtom
        87654321
    

Príklady práce s binárnym súborom

Uvedieme niekoľko užitočných príkladov.

  • obsah binárneho súboru ako 16-ové hodnoty:

    def to_hex(subor, pocet=None):           # pocet urcuje pocet precitanych bajtov
        with open(subor, 'rb') as file:
            return ' '.join(f'{x:02X}' for x in file.read(pocet))
    
    >>> to_hex('python.exe', 1000)
    

    to isté by sa dalo zapísať aj takto pomocou metódy hex:

    def to_hex(subor, pocet=None):
        with open(subor, 'rb') as file:
            return file.read(pocet).hex(' ')
    
  • obsah binárneho súboru so zobrazitelnými znakmi:

    def to_str(subor, pocet=None):           # pocet urcuje pocet precitanych bajtov
        with open(subor, 'rb') as file:
            return ''.join(chr(x) if 32<=x<127 else '.' for x in file.read(pocet))
    
    >>> to_str('python.exe', 1000)
    
  • zapísať do súboru zoznam dvojbajtových čísel (kladné čísla od 0 do 65535) - čísla sa zapisujú tak, že prvý bajt z dvojice je ten vyšší:

    def zapis_zoznam(subor, zoznam):
        with open(subor, 'wb') as file:
            for x in zoznam:
                file.write(x.to_bytes(2, 'big'))
    
    >>> zapis_zoznam('zoznam.dat', range(1000, 2000, 7))
    
  • prečítať zo súboru zoznam dvojbajtových čísel (kladné čísla od 0 do 65535) - čísla sa čítajú tak, že prvý bajt z dvojice je ten vyšší:

    def citaj_zoznam(subor):
        zoznam = []
        with open(subor, 'rb') as file:
            x = file.read(2)
            while x != b'':
                zoznam.append(int.from_bytes(x, 'big'))
                x = file.read(2)
        return zoznam
    
    >>> zoz = citaj_zoznam('zoznam.dat')
    >>> zoz
    
  • zapísať do súboru zoznam znakových reťazcov - každý reťazec sa zapíše tak, že najprv je v jednom bajte dĺžka reťazca v bajtoch (po zakódovaní do bytes) a za tým samotný reťazec:

    def zapis_retazce(subor, retazce):
        with open(subor, 'wb') as file:
            for r in retazce:
                r = r.encode()
                file.write(bytes((len(r),))+r)
    
    >>> ret = ('prvý', 'druhý\na tretí', '', '\n\n\n')
    >>> zapis_retazce('retazce.dat', ret)
    
  • prečítať zo súboru zoznam znakových reťazcov - každý reťazec sa prečíta tak, že najprv je v jednom bajte dĺžka reťazca v bajtoch (po zakódovaní do bytes) a za tým samotný reťazec:

    def citaj_retazce(subor):
        ret = []
        with open(subor, 'rb') as file:
            dlzka = file.read(1)
            while dlzka != b'':
                ret.append(file.read(dlzka[0]).decode())
                dlzka = file.read(1)
        return ret
    
    >>> r = citaj_retazce('retazce.dat')
    >>> r
    
  • vytvoriť kópiu súboru:

    def kopia(subor1, subor2):
        with open(subor1, 'rb') as file1, open(subor2, 'wb') as file2:
            file2.write(file1.read())
    
    >>> kopia('subor.dat', 'subor1.dat')
    
  • skontrolovať, či majú dva súboru identický obsah:

    def zhoda(subor1, subor2):
        with open(subor1, 'rb') as file1, open(subor2, 'rb') as file2:
            return file1.read() == file2.read()
    
    >>> zhoda('subor.dat', 'subor1.dat')
    

Cvičenia


iterátory


  1. * Vytvor triedu Fib s metódami __iter__ a __next__, ktora postupne generuje n-prvkovú postupnosť fibonacciho čísel:

    class Fib:
        def __init__(self, n):
            ...
    
        def __iter__(self):
            ...
    
        def __next__(self):
            ...
    

    Test:

    for f in Fib(100):
        print(f, end=' ')
        if f > 20:
            break
    

    vypíše:

    0 1 1 2 3 5 8 13 21
    
  2. * Vytvor iterátor, ktorý bude generovať nekonečnú postupnosť súčtov celých čísel, t.j. postupne 1, 1+2, 1+2+3, 1+2+3+4, … . Použi __getitem__:

    class Sucty:
        def __init__(self):
            ...
    
        def __getitem__(self, ix):
            ...
    

    Napríklad test:

    sucty = iter(Sucty())
    for i in range(5):
        print(next(sucty), end=' ')
    

    vypíše:

    1 3 6 10 15
    

    Samozrejme, že to má fungovať pre ľubovoľné počty súčtov.


  1. * Funkcia test() prechádza pomocou dvoch for-cyklov dve iterovateľné hodnoty (napríklad množiny):

    def test(m1, m2):
        for i in m1:
            for j in m2:
                print(i, j, i + j)
    
    test({'a', 'b'}, {'x', 'y', 'z'})
    

    Zapíš funkciu test1, ktorá urobí rovnaké cykly ako test, ale for-cykly nahradí pomocou iterátorov a while-cyklov.


  1. Pre triedu SpajanyZoznam (z predchádzajúcej prednášky) s metódou __getitem__() odmeraj čas pre väčší spájaný zoznam, napríklad:

    zoz = SpajanyZoznam(range(10000))
    print('pocitam...')
    sucet = 0
    for p in zoz:
        sucet += p
    print(p)
    

    Namiesto __getitem__() definuj metódy __iter__() a __next__() tak, aby sa prvky zoznamu dali prechádzať for-cyklom, otestuj funkčnosť a porovnaj rýchlosť s predchádzajúcim testom. Mal bz si si všimnúť výrazný rozdiel v čase behu oboch testov.


generátory


  1. Zisti najprv bez počítača, čo sa vypíše:

    def cele_cisla():
        i = 1
        while True:
            yield i
            i = i + 1
    
    def mocniny():
        for i in cele_cisla():
            yield i * i
    
    def zisti(n, post):
        post = iter(post)
        vysl = []
        try:
            for i in range(n):
                vysl.append(next(post))
        except StopIteration:
            pass
        return vysl
    
    print(zisti(5, mocniny()))
    

  1. * Napíš funkciu grange(start, stop, krok), ktorá bude generátorom (použije yield) a bude generovať rovnakú postupnosť celých čísel ako štandardný range(). Môžeš predpokladať, že parameter krok je väčší ako 0 (hoci výzvou by mohol byť aj záporný krok). Samozrejme, že vo funkcii nesmieš použiť funkciu range(). Napríklad:

    >>> tuple(grange(3, 50, 7))
        (3, 10, 17, 24, 31, 38, 45)
    

    Generátor by mal fungovať aj pre desatinné čísla:

    >>> tuple(grange(1, 5, 0.5))
        (1, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5)
    

  1. * Vytvor a otestuj tieto štyri verzie funkcie mocniny(n), ktorá

    • vráti postupnosť (list) druhých mocnín [1, 4, 9, 16, ..., n**2] vytvorenú pomocou for-cyklu a metódy append()

    • vráti postupnosť (list) ale vytvorené pomocou generátorovej notácie (napríklad [... for i in ...])

    • túto postupnosť vráti ako generátorovú funkciu (použitím yield)

    • túto postupnosť vráti ako generátorovú funkciu (použitím generátorového zápisu (... for i in ...) bez yield)


  1. Zapíš dve verzie funkcie map(funkcia, post), ktorá vráti prvky postupnosti post prerobené funkciou funkcia (podobne ako to robí štandardná funkcia map(), ale rieš to bez tejto funkcie)

    • výsledok vytvor najprv ako zoznam (list)

    • potom výsledok ako generátor

    • otestuj, ako sa bude správať map(), ak druhým parametrom je nejaký generátor

    • porovnaj so štandardnou funkciou map()


  1. Zapíš dve verzie funkcie filter(funkcia, post), ktorá vráti len tie prvky postupnosti post, pre ktoré je splnená logická funkcia

    • výsledok najprv ako zoznam (list)

    • výsledok ako generátor

    • otestuj, ako sa bude správať filter(), ak druhým parametrom je nejaký generátor

    • porovnaj so štandardnou funkciou filter()


  1. * Zapíš funkciu zdvoj(gen), ktorá vygeneruje každý prvok 2-krát za sebou - funkcia vráti generátor

    • vyskúšaj nielen s parametrom typu generátor, ale napríklad aj so zoznamom alebo s reťazcom

    • na riešenie asi použiješ funkcie iter a next a potom aj príkaz yield

    Napríklad:

    >>> g = zdvoj(i**2 for i in range(1, 5))
    >>> g
        <generator object zdvoj at 0x022A6828>
    >>> list(g)
        [1, 1, 4, 4, 9, 9, 16, 16]
    >>> zdvoj('Python')
        ...
    >>> zdvoj([2, 3, 5])
        ...
    

  1. * Zapíš dve verzie funkcie spoj(gen1, gen2), ktorá vygeneruje (vráti ako generátor) najprv všetky prvky gen1 potom všetky prvky gen2

    • vyskúšaj nielen s parametrami typu generátor, ale napríklad aj so zoznamami a reťazcami

    • na riešenie môžeš použiť (ale nemusiš) funkcie iter a next a zrejme aj príkaz yield

    • zapíš verziu funkcie spoj(*gen), v ktorej sa spája ľubovoľne veľa generátorov

    Napríklad:

    >>> g = spoj(iter(range(5)), iter(range(10, 0, -2)))
    >>> g
        <generator object spoj at 0x00A823C0>
    >>> print(*g)
        0 1 2 3 4 10 8 6 4 2
    >>> g = spoj(iter(range(5)), iter('ahoj'), iter(range(10, 0, -2)))
    >>> print(*g)
        0 1 2 3 4 a h o j 10 8 6 4 2
    

  1. * Zapíš tri verzie funkcie mix(gen1, gen2), ktorá generuje prvky na striedačku - ak v jednom skončí skôr, tak už berie len zvyšné druhého

    • najprv s pomocným zoznamom: prvý generátor najprv presype prvky do zoznamu, a potom počas prechodu druhým generátorom dáva aj prvky z pomocného zoznamu

    • bez pomocného zoznamu len pomocou štandardnej funkcie next()

    • porozmýšľaj nad verziou mix(*gen), v ktorej sa mixuje ľubovoľne veľa generátorov

    Napríklad:

    >>> print(*mix(iter('PYTHON'), iter(range(4)), iter('ahoj')))
        P 0 a Y 1 h T 2 o H 3 j O N
    

binárne súbory


  1. * Napíš dve funkcie vyrob(meno_suboru, n, pocet) a citaj(meno_suboru, n). Prvá z nich vygeneruje binárny súbor s pocet náhodných celých n-bajtových čísel (čísla začínajú najvyšším bajtom). Náhodné hodnoty nech sú napríklad z rozsahu range(0, 256**n, 100).

    Druhá funkcia citaj() prečíta súbor vytvorený funkciou vyrb() a vráti jeho prvky v tvare zoznamu (list). Svoje riešenie otestuj pre rôzne n.


  1. V binárnom súbore 'cisla.dat' je uložená postupnosť celých n-bajtových čísel (čísla začínajú najvyšším bajtom). Napíš funkciu vyhod(meno_suboru='cisla.dat', n=1, hodnota=0), ktorá z daného súboru vyhodí všetky výskyty zadanej hodnoty. Napríklad, volanie vyhod('cisla4.dat', 4, 1000) vyhodí z daného súboru 4-bajtových celých čísel všetky výskytu hodnoty 1000. Svoje riešenie otestuj s rôznymi binárnymi súbormi, v ktorých je rôzna veľkosť zapísaných celých čísel (napríklad pre jednobajtové, dvojbajtové, štvorbajtové).


  1. * Napíš funkciu pocet_bajtov(x), ktorá pre kladné celé číslo x zistí minimálny počet bajtov, ktoré dané číslo zaberá. Napríklad:

    >>> pocet_bajtov(0)
        1
    >>> pocet_bajtov(200)
        1
    >>> pocet_bajtov(2000)
        2
    

  1. Napíš funkciu zapis_zoznam(meno_suboru, zoznam), ktorá do binárneho súboru zapíše prvky daného zoznamu (list). Prvkami sú buď kladné celé čísla alebo znakové reťazce. Navrhni formát binárneho súboru tak, aby sa dal spätne prečítať a vytvoriť pôvodný zoznam: asi najlepšie tak, že v ňom bude pre každý prvok informácia o tom, či je to celé číslo alebo znakový reťazec (napríklad znakom 'i' alebo 's') a tiež počet bajtov, ktoré zaberá (môžeš predpokladať, že tento počet bude do 255). Otestuj, napríklad pre:

    zapis_zoznam('zoznam.dat', ['Abc', 42, '', '*'*100, 7**100])
    

    Ďalej napíš aj funkciu citaj_zoznam(meno_suboru), ktorá vráti prečítaný zoznam z binárneho súboru z funkcie zapis_zoznam().


  1. Napíš funkciu zapis_desatinne(meno_suboru, zoznam), ktorá do binárneho súboru zapíše zoznam desatinných čísel (float). Navrhni takú reprezentáciu, aby sa takýto súbor dal naspäť prečítať. Otestuj, napríklad:

    zapis_desatinne('float.dat', list(1/i for i in range(1, 8)))
    

    Ďalej napíš aj funkciu citaj_desatinne(meno_suboru), ktorá vráti prečítaný zoznam čísel z binárneho súboru z funkcie zapis_desatinne().


4. Týždenný projekt


Toto domáce zadanie bude riešiť takúto úlohu:

  • v súbore sa nachádzajú informácie o študentoch nejakej školy (osobné číslo, meno a priezvisko, zoznam udelených známok);

  • tento súbor treba prečítať a vytvoriť z neho jednosmerný spájaný zoznam (metoda read), v ktorom bude v každom vrchole informácia o jednom študentovi; vrcholy budú v spájanom zozname v tom poradí, v akom boli v súbore;

  • z tohto zoznamu budeme vedieť nájsť študenta (metóda __getitem__) podľa zadaného osobného čísla

  • tento zoznam bude treba vedieť zapísať späť do súboru (metóda write), ale v inom poradí:

    • metóda remove_min nájde študenta s najmenším osobným číslom, tento vrchol zo spájaného zoznamu odstráni a vráti ho ako výsledok funkcie

    • údaje tohto študenta zapíše v požadovanom formáte do súboru

    • toto opakuje, kým nebude spájaný zoznam prázdny

Formát súboru so študentmi nebude textový, ale binárny. Pre každého študenta bude v súbore postupnosť takýchto bajtov:

  • najprv 4 bajty s osobným číslom (osobné číslo je celé číslo z intervalu <0, 4294967295>, teda 256**4-1), kde najnižší bajt je prvý v štvorici, napríklad číslo 74565 bude v štyroch bajtoch ako (69, 35, 1, 0), alebo v šestnástkovej sústave ako (0x45, 0x23, 0x01, 0x00)

  • potom nasleduje postupnosť znakov (postupnosť bajtov s ASCII-kódmi, znaky nebudú obsahovať diakritiku), pričom prvý bajt postupnosti obsahuje dĺžku reťazca, napríklad meno a priezvisko 'Abc Def', bude uložené v 8 bajtoch: (7, 65, 98, 99, 32, 68, 101, 102), čo vieme zapísať aj v 16-ovej sústave: (0x07, 0x41, 0x62, 0x63, 0x20, 0x44, 0x65, 0x66); zrejme takéto reťazce môžu mať maximálnu dĺžku 255 znakov; ASCII-kódy znakov budú len z intervalu <32, 126>

  • na záver je to postupnosť známok v tvare čísel od 1 do 6, ktorá je ukončená číslom 0, pričom 1 zodpovedá známke A, 2 zodpovedá B, atď. až 6 zodpovedá Fx, 0 tu označuje koniec postupnosti a nereprezentuje známku, napríklad postupnosť (3, 1, 6, 4, 0) popisuje 4 známky (C, A, Fx, D); ak budeme niekedy potrebovať vypočítať priemer, tak použijeme prepočet, v ktorom známka A má hodnotu 1, známka B má hodnotu 1.5, známka C má hodnotu 2, atď. až známka Fx má hodnotu 4; potom priemer známok je (2+1+4+2.5)/4, teda 2.375

  • ak sa v súbore hneď na začiatku objaví táto postupnosť bajtov (zapísali sme ju v 16-ovej sústave):

    0x45, 0x23, 0x01, 0x00, 0x07, 0x41, 0x62, 0x63, 0x20, 0x44, 0x65, 0x66, 0x03, 0x01, 0x06, 0x04, 0x00
    

    týchto 17 bajtov reprezentuje informácie o jednom študentovi s osobným číslom 74565, s menom a priezviskom 'Abc Def' a so známkami (C, A, Fx, D); za týmito bajtami môže v súbore nasledovať ďalšia postupnosť bajtov, ktorá popisuje ďalšieho študenta

Napíš modul s menom riesenie.py, ktorý bude obsahovať jedinú triedu s ďalšou vnorenou podtriedou a týmito metódami:

class LinkedList:
    class Student:
        def __init__(self, number, name, grades):  # osobné číslo, meno a priezvisko, zoznam známok
            self.number = number                   # celé číslo z <0, 4294967295>
            self.name = name                       # znakový reťazec
            self.grades = grades                   # n-tica (tuple) celých čísel z <1, 6>
            self.next = None

        def __repr__(self):
            return f'Student({self.number}, {self.name!r}, {self.grades})'

    def __init__(self):
        self.zac = self.kon = None
        ...

    def read(self, file_name):
        ...

    def write(self, file_name):
        ...

    def remove_min(self):
        ...
        return ...

    def __getitem__(self, number):
        ...
        return None

    def __len__(self):
        ...
        return 0

Trieda LinkedList implementuje jednosmerný spájaný zoznam študentov, pričom metódy triedy by mali mať takúto funkčnosť (môžeš si dodefinovať aj ďalšie pomocné metódy):

  • metóda __init__() inicializuje atribúty zac a kon pre referencie na začiatok a koniec zoznamu;

  • metóda read(file_name) otvorí binárny súbor a informácie o študentoch pridá na koniec momentálneho spájaného zoznamu; zrejme by takto mohol z viacerých súborov vyrobiť jeden spájaný zoznam;

  • metóda write(file_name) zapíše kompletný spájaný zoznam do binárneho súboru; použije na to metódu remove_min, pomocou ktorej získa študenta s najmenším osobným číslom, študenta zapíše do súboru; ak túto metódu bude volať, kým nebude spájaný zoznam prázdny, zapíše do súboru všetkých študentov v usporiadanom poradí;

  • metóda remove_min() vyhľadá študenta s najmenším osobným číslom, tohto študenta vyhodí zo spájaného zoznamu a samotný vrchol (typu self.Student) vráti ako výsledok funkcie; ak bol zoznam už prázdny, funkcia vráti None;

  • metóda __getitem__(number) vráti informácie o študentovi s daným osobným číslom v tvare dvojice (meno, priemer), kde meno je znakový reťazec s menom a priezviskom študenta, priemer je priemer jeho známok; ak je zoznam študentových známok prázdny, jeho priemer je 0; ak študenta s daným osobným číslom nenajde, funkcia vráti None;

  • metóda __len__() vráti aktuálny počet prvkov zoznamu.

Obmedzenia

  • svoje riešenie odovzdaj v súbore riesenie.py, pričom sa v ňom bude nachádzať len jedna definícia triedy LinkedList, trieda Student bude vnorená v triede LinkedList

  • prvé tri riadky tohto súboru budú obsahovať:

    # 4. zadanie: binarny subor
    # autor: Janko Hrasko
    # datum: 15.3.2024
    
  • zrejme ako autora uvedieš svoje meno

  • atribúty zac a kon v triede LinkedList musia obsahovať referencie na začiatok a koniec zoznamu

  • tvoj program by nemal počas testovania testovačom nič vypisovať (žiadne testovacie print())

Testovanie

Keď budeš spúšťať svoje riešenie na počítači, môžeš do súboru riesenie.py pridať testovacie riadky, ktoré ale testovač vidieť nebude, napríklad, ak subor1.dat obsahuje takúto postupnosť bajtov (zapísali sme ich v 16-ovej sústave a rozsekali sme ich tak, aby bolo lepšie vidieť informácie o štyroch študentoch):

4523010007416263204465660301060400
0004000007476820496A6B6C0200
B17F3905094D204E6F707172737400
87D61200075576777879205A0101010300

môžeme zapísať takýto test:

if __name__ == '__main__':
    zoz = LinkedList()
    zoz.read('subor1.dat')
    print('pocet =', len(zoz))
    p = zoz.zac
    while p:
        print(p)
        p = p.next
    print()
    for cislo in 74565, 87654321, 8765432:
        print(f'student[{cislo}] =', zoz[cislo])
    print('min =', zoz.remove_min())
    print('pocet po remove_min =', len(zoz))
    zoz.write('subor2.dat')
    print('pocet po write =', len(zoz))

Tento test by mal vypísať:

pocet = 4
Student(74565, 'Abc Def', (3, 1, 6, 4))
Student(1024, 'Gh Ijkl', (2,))
Student(87654321, 'M Nopqrst', ())
Student(1234567, 'Uvwxy Z', (1, 1, 1, 3))

student[74565] = ('Abc Def', 2.375)
student[87654321] = ('M Nopqrst', 0)
student[8765432] = None
min = Student(1024, 'Gh Ijkl', (2,))
pocet po remove_min = 3
pocet po write = 0

Projekt riesenie.py odovzdaj na úlohový server https://list.fmph.uniba.sk/.