17. Výnimky


Zapíšme funkciu, ktorá prečíta celé číslo zo vstupu:

def cislo():
    vstup = input('zadaj cislo: ')
    return int(vstup)
>>> cislo()
zadaj cislo: 234
234

Toto funguje, len ak zadáme korektný reťazec celého čísla. Spadne to s chybovou správou pri zle zadanom vstupe:

>>> cislo()
zadaj cislo: 234a
...
ValueError: invalid literal for int() with base 10: '234a'

Aby takýto prípad nenastal, vložíme pred volanie funkcie int() test, napríklad takto:

def test_cele_cislo(retazec):
    for znak in retazec:
        if znak not in '0123456789':
            return False
    return True

def cislo():
    vstup = input('zadaj cislo: ')
    if test_cele_cislo(vstup):
        return int(vstup)
    print('chybne zadane cele cislo')

Pripadne s opakovaným vstupom pri chybne zadanom čísle (hoci aj tento test test_cele_cislo() nie je dokonalý a niekedy prejde, aj keď nemá):

def cislo():
    while True:
        vstup = input('zadaj cislo: ')
        if test_cele_cislo(vstup):
            return int(vstup)
        print('*** chybne zadane cele cislo ***')

Takto ošetrený vstup už nespadne, ale oznámi chybu a pýta si nový vstup, napríklad:

>>> cislo()
zadaj cislo: 234a
*** chybne zadane cele cislo ***
zadaj cislo: 234 a
*** chybne zadane cele cislo ***
zadaj cislo: 234
234
>>>

try - except

Python umožňuje aj iný spôsob riešenia takýchto situácií: chybe sa nebudeme snažiť predísť, ale keď vznikne, tak ju „ošetríme“. Využijeme novú programovú konštrukciu try - except. Jej základný tvar je:

try:
    '''blok príkazov'''
except MenoChyby:
    '''ošetrenie chyby'''

Konštrukcia sa skladá z dvoch častí:

  • príkazy medzi try a except

  • príkazmi za except

Blok príkazov medzi try a except bude teraz Python spúšťať „opatrnejšie“, t.j. ak pri ich vykonávaní nastane uvedená chyba (meno chyby za except), vykonávanie bloku príkazov sa okamžite ukončí a pokračuje sa príkazmi za except, pritom Python zruší chybový stav, v ktorom sa práve nachádzal. Ďalej sa pokračuje v príkazoch za touto konštrukciou.

Ak pri opatrnejšom vykonávaní bloku príkazov uvedená chyba nenastane, tak príkazy za except sa preskočia a normálne sa pokračuje v príkazoch za konštrukciou.

Ak pri opatrnejšom vykonávaní bloku príkazov nastane iná chyba ako je uvedená v riadku except, tak táto konštrukcia túto chybu nespracuje, ale pokračuje sa tak, ako sme boli zvyknutí doteraz, t.j. celý program spadne a IDLE o tom vypíše chybovú správu.

Ukážme to na predchádzajúcom príklade (pomocnú funkciu test_cele_cislo() teraz už nepotrebujeme):

def cislo():
    while True:
        vstup = input('zadaj cislo: ')
        try:
            return int(vstup)
        except ValueError:
            print('*** chybne zadane cele cislo ***')

To isté by sa dalo zapísať aj niekoľkými inými spôsobmi, napríklad:

def cislo():
    while True:
        try:
            return int(input('zadaj cislo: '))
        except ValueError:
            print('*** chybne zadane cele cislo ***')
def cislo():
    while True:
        try:
            vysledok = int(input('zadaj cislo: '))
            break
        except ValueError:
            print('*** chybne zadane cele cislo ***')
    return vysledok
def cislo():
    ok = False
    while not ok:
        try:
            vysledok = int(input('zadaj cislo: '))
            ok = True
        except ValueError:
            print('*** chybne zadane cele cislo ***')
    return vysledok

Na chybové prípady odteraz nemusíme pozerať ako na niečo zlé, ale ako na výnimočné prípady (výnimky, t.j. exception), ktoré vieme veľmi šikovne vyriešiť. Len neošetrená výnimka spôsobí spadnutie nášho programu. Často to bude znamenať, že sme niečo zle naprogramovali, alebo sme nedomysleli nejaký špeciálny prípad.

Na predchádzajúcom príklade sme videli, že odteraz bude pre nás dôležité meno chyby (napríklad ako v predchádzajúcom príklade ValueError). Mená chýb nám prezradí Python, keď vyskúšame niektoré konkrétne situácie, napríklad:

>>> 1+'2'
...
TypeError: unsupported operand type(s) for +: 'int' and 'str'
>>> 12/0
...
ZeroDivisionError: division by zero
>>> x+1
...
NameError: name 'x' is not defined
>>> open('')
...
FileNotFoundError: [Errno 2] No such file or directory: ''
>>> [1,2,3][10]
...
IndexError: list index out of range
>>> 5()
...
TypeError: 'int' object is not callable
>>> ''.x
...
AttributeError: 'str' object has no attribute 'x'
>>> 2**10000/1.
...
OverflowError: int too large to convert to float
>>> import x
...
ImportError: No module named 'x'
>>> def t(): x += 1
>>> t()
...
UnboundLocalError: local variable 'x' referenced before assignment

Všimnite si, že Python za meno chyby vypisuje aj nejaký komentár, ktorý nám môže pomôcť pri pochopení dôvodu chyby, resp. pri ladení. Tento text je ale mimo mena chyby, v except riadku ho nepíšeme. Takže, ak chceme odchytiť a spracovať nejakú konkrétnu chybu, jej meno si najjednoduchšie zistíme v dialógovom režime v IDLE.

Spracovanie viacerých výnimiek

Pomocou try a except môžeme zachytiť aj viac chýb ako jednu. V ďalšom príklade si funkcia vyžiada celé číslo, ktoré bude indexom do nejakého zoznamu. Funkcia potom vypíše hodnotu prvku s týmto indexom. Môžu tu nastať dve rôzne výnimky:

  • ValueError pre zle zadané celé číslo indexu

  • IndexError pre index mimo rozsah zoznamu

Zapíšme funkciu:

def zisti(zoznam):
    while True:
        try:
            vstup = input('zadaj index: ')
            index = int(vstup)
            print('prvok zoznamu =', zoznam[index])
            break
        except ValueError:
            print('*** chybne zadane cele cislo ***')
        except IndexError:
            print('*** index mimo rozsah zoznamu ***')

otestujeme:

>>> zisti(['prvy', 'druhy', 'treti', 'stvrty'])
zadaj index: 22
*** index mimo rozsah zoznamu ***
zadaj index: 2.
*** chybne zadane cele cislo ***
zadaj index: 2
prvok zoznamu = treti
>>>

To isté by sme dosiahli aj vtedy, keby sme to zapísali pomocou dvoch vnorených príkazov try:

def zisti(zoznam):
    while True:
        try:
            try:
                vstup = input('zadaj index: ')
                index = int(vstup)
                print('prvok zoznamu =', zoznam[index])
                break
            except ValueError:
                print('*** chybne zadane cele cislo ***')
        except IndexError:
            print('*** index mimo rozsah zoznamu ***')

Zlúčenie výnimiek

Niekedy sa môže hodiť, keď máme pre rôzne výnimky spoločný kód. Za except môžeme uviesť aj viac rôznych mien výnimiek, ale musíme ich uzavrieť do zátvoriek (urobiť z nich tuple), napríklad:

def zisti(zoznam):
    while True:
        try:
            print('prvok zoznamu =', zoznam[int(input('zadaj index: '))])
            break
        except (ValueError, IndexError):
            print('*** chybne zadany index zoznamu ***')
>>> zisti(['prvy', 'druhy', 'treti', 'stvrty'])
zadaj index: 22
*** chybne zadany index zoznamu ***
zadaj index: 2.
*** chybne zadany index zoznamu ***
zadaj index: 2
prvok zoznamu = treti
>>>

Uvedomte si, že pri takomto zlučovaní výnimiek môžeme stratiť detailnejšiu informáciu o tom, čo sa v skutočnosti udialo.

Príkaz try - except môžeme použiť aj bez uvedenia mena chyby: vtedy to označuje zachytenie všetkých typov chýb, napríklad:

def zisti(zoznam):
    while True:
        try:
            print('prvok zoznamu =', zoznam[int(input('zadaj index: '))])
            break
        except:
            print('*** chybne zadany index zoznamu ***')

Takýto spôsob použitia try - except sa ale neodporúča, skúste sa ho vyvarovať! Jeho používaním môžete ušetriť zopár minút pri hľadaní všetkých typov chýb, ktoré môžu vzniknúť pri vykonávaní daného bloku príkazov. Ale skúsenosti ukazujú, že môžete zase stratiť niekoľko hodín pri hľadaní chýb v takýchto programoch. Veľmi často sú takéto bloky try - except bez uvedenia výnimky zdrojom veľmi nečakaných chýb.

Ako funguje mechanizmus výnimiek

Kým sme nepoznali výnimky, ak nastala nejaká chyba v našej funkcii, dostali sme nejaký takýto výpis:

>>> fun1()
Traceback (most recent call last):
  File "<pyshell#9>", line 1, in <module>
    fun1()
  File "p.py", line 13, in fun1
    fun2()
  File "p.py", line 16, in fun2
    fun3()
  File "p.py", line 19, in fun3
    fun4()
  File "p.py", line 22, in fun4
    int('x')
ValueError: invalid literal for int() with base 10: 'x'

Python pri výskyte chyby (t.j. nejakej výnimky) hneď túto chybu nevypisuje, ale zisťuje, či sa nevyskytla v bloku príkazu try - except. Ak áno a meno chyby zodpovedá menu v except, vykoná definované príkazy pre túto výnimku.

Ak na danom mieste neexistuje obsluha tejto výnimky, vyskočí z momentálnej funkcie a zisťuje, či jej volanie v nadradenej funkcii nebolo chránené príkazom try - except. Ak áno, vykoná čo treba a na tomto mieste pokračuje ďalej, akoby sa žiadna chyba nevyskytla.

Ak ale ani v tejto funkcii nie je kontrola pomocou try - except, vyskakuje o úroveň vyššie a vyššie, až kým nepríde na najvyššiu úroveň, teda do dialógu IDLE. Keďže nik doteraz nezachytil túto výnimku, IDLE ju vypíše v nám známom tvare. V tomto výpise vidíme, ako sa Python „vynáral“ vyššie a vyššie.

Práca so súbormi

Pri práci so súbormi výnimky vznikajú veľmi často a nie je jednoduché ošetriť všetky situácie pomocou podmienených príkazov. Najčastejšou chybou je neexistujúci súbor:

try:
    with open('x.txt') as subor:
        cislo = int(subor.readline())
except FileNotFoundError:
    print('*** neexistujuci subor ***')
    cislo = 0
except (ValueError, TypeError):
    print('*** prvy riadok suboru neobsahuje cele cislo ***')
    cislo = 10

Prípadne sa môže hodiť pomocná funkcia, ktorá zistí, či súbor s daným menom existuje:

def existuje(meno_suboru):
    try:
        with open(meno_suboru):
            return True
    except (TypeError, OSError, FileNotFoundError):
        return False

Uvedomte si ale, že táto funkcia, aby zistila, či súbor existuje, ho otvorí a okamžite aj zatvorí. Z tohto dôvodu nasledovný kód nie je veľmi efektívny:

if existuje('x.txt'):
    with open('x.txt') as t:
        obsah = t.read()
else:
    obsah = ''

Vyvolanie výnimky

V niektorých situáciách sa nám môže hodiť vyvolanie vzniknutej chyby aj napriek tomu, že ju vieme zachytiť príkazom try - except. Slúži na to nový príkaz raise, ktorý má niekoľko variantov. Prvý z nich môžete vidieť v upravenej verzii funkcie cislo(). Funkcia sa najprv 3-krát pokúsi prečítať číslo zo vstupu, a ak sa jej to napriek tomu nepodarí, rezignuje a vyvolá známu chybu ValueError:

def cislo():
    pokus = 0
    while True:
        try:
            return int(input('zadaj cislo: '))
        except ValueError:
            pokus += 1
            if pokus >= 3:
                raise
            print('*** chybne zadane cele cislo ***')
>>> cislo()
zadaj cislo: jeden
*** chybne zadane cele cislo ***
zadaj cislo: dva
*** chybne zadane cele cislo ***
zadaj cislo: tri
...
ValueError: invalid literal for int() with base 10: 'tri'

Pomocou príkazu raise môžeme vyvolať nielen práve zachytenú výnimku, ale môžeme vyvolať ľubovoľnú inú chybu aj s vlastným komentárom, ktorý sa pri nej vypíše, napríklad:

raise ValueError('chybne zadane cele cislo')
raise ZeroDivisionError('delenie nulou')
raise TypeError('dnes sa ti vobec nedari')

Príklad s metódou index()

Poznáme už metódu index(), ktorá v zozname (typ list, tuple alebo str) nájde prvý výskyt nejakej hodnoty. Metóda je zaujímavá tým, že vyvolá výnimku ValueError, ak sa táto hodnota v zozname nenachádza. Kým sme nepoznali odchytávanie výnimiek, väčšinou sme museli najprv kontrolovať, či sa tam príslušný prvok nachádza a až potom, keď sa nachádza, volali sme index(), napríklad:

def nahrad(zoznam, h1, h2):
    if h1 in zoznam:
        i = zoznam.index(h1)
        zoznam[i] = h2

Pomocou try - except to vyriešime výrazne efektívnejšie:

def nahrad(zoznam, h1, h2):
    try:
        i = zoznam.index(h1)
        zoznam[i] = h2
    except ValueError:
        pass

Táto metóda index(), ktorá funguje pre jednorozmerné zoznamy, nás môže inšpirovať aj na úlohu, v ktorej budeme hľadať indexy do dvojrozmernej tabuľky. Napíšme funkciu hladaj(zoznam, hodnota), ktorá hľadá prvý výskyt danej hodnoty v dvojrozmernom zozname a ak taký prvok nájde, vráti jeho číslo riadku a číslo stĺpca. Ak sa tam taký prvok nenachádza, funkcia by mala vyvolať rovnakú výnimku, ako to robila pôvodná metóda index(), t.j. ValueError: hodnota is not in list. Zapíšme riešenie dvoma vnorenými cyklami:

def hladaj(zoznam, hodnota):
    for r in range(len(zoznam)):
        for s in range(len(zoznam[r])):
            if zoznam[r][s] == hodnota:
                return r, s
    raise ValueError(f'{hodnota!r} is not in list')

alebo to isté zápis pomocou štandardnej funkcie enumerate():

def hladaj(zoznam, hodnota):
    for r, riadok in enumerate(zoznam):
        for s, prvok in enumerate(riadok):
            if prvok == hodnota:
                return r, s
    raise ValueError(f'{hodnota!r} is not in list')

Hoci je toto správne riešenie, vieme ho zapísať aj efektívnejšie pomocou volania metódy index():

def hladaj(zoznam, hodnota):
    for r, riadok in enumerate(zoznam):
        try:
            s = riadok.index(hodnota)
            return r, s
        except ValueError:
            pass
    raise ValueError(f'{hodnota!r} is not in list')

Ak si ale uvedomíme, že neúspešné hľadanie prvku v r-tom riadku zoznamu pomocou index() vyvolá presne tú istú chybu, ktorú sme zachytili a potom znovu vyvolali, môžeme to celé skrátiť takto:

def hladaj(zoznam, hodnota):
    for r, riadok in enumerate(zoznam):
        try:
            s = riadok.index(hodnota)
            return r, s
        except ValueError:
            if r == len(zoznam)-1:     # posledný prechod for-cyklom
                raise

Teda zachytená chyba ValueError v poslednom riadku dvojrozmerného zoznamu označuje, že sa hodnota nenachádza v žiadnom riadku zoznamu a teda sa opätovne vyvolá zachytená chyba. (Zamyslite sa, ako bude toto riešenie fungovať pre prázdny dvojrozmerný zoznam, teda zoznam, ktorý neobsahuje ani jeden riadok).

Vytváranie vlastných výnimiek

Keď chceme vytvoriť vlastný typ výnimky, musíme vytvoriť novú triedu odvodenú od základnej triedy Exception. Môže to vyzerať napríklad takto:

class MojaChyba(Exception): pass

Príkaz pass tu znamená, že nedefinujeme nič nové oproti základnej triede Exception. Použiť to môžeme napríklad takto:

def podiel(p1, p2):
    if not isinstance(p1, int):
        raise MojaChyba('prvy parameter nie je cele cislo')
    if not isinstance(p2, int):
        raise MojaChyba('druhy parameter nie je cele cislo')
    if p2 == 0:
        raise MojaChyba('neda sa delit nulou')
    return p1 // p2
>>> podiel(22, 3)
7
>>> podiel(2.2, 3)
...
MojaChyba: prvy parameter nie je cele cislo
>>> podiel(22, 3.3)
...
MojaChyba: druhy parameter nie je cele cislo
>>> podiel(22, 0)
...
MojaChyba: neda sa delit nulou
>>>

Kontrola pomocou assert

Ďalší nový príkaz assert slúži na kontrolu nejakej podmienky: ak táto podmienka nie je splnená, vyvolá sa výnimka AssertionError aj s uvedeným komentárom. Tvar tohto príkazu je:

assert podmienka, 'komentár'

Toto sa často používa pri ladení, keď potrebujeme mať istotu, že je splnená nejaká konkrétna podmienka (vlastnosť) a v prípade, že nie je, chceme radšej aby program spadol, ako pokračoval ďalej. Prepíšme funkciu podiel() tak, že namiesto if a raise zapíšeme volanie assert:

def podiel(p1, p2):
    assert isinstance(p1, int), 'prvy parameter nie je cele cislo'
    assert isinstance(p2, int), 'druhy parameter nie je cele cislo'
    assert p2 != 0, 'neda sa delit nulou'
    return p1 // p2
>>> podiel(2.2, 3)
...
AssertionError: prvy parameter nie je cele cislo
>>> podiel(22, 3.3)
...
AssertionError: druhy parameter nie je cele cislo
>>> podiel(22, 0)
...
AssertionError: neda sa delit nulou

Príklad s triedou Ucet

Na minulých cvičeniach ste riešili príklad, v ktorom ste definovali triedu Ucet, resp. UcetHeslo aj jej metódy vklad() a vyber(). Ukážme riešenie trochu zjednodušeného zadania len s jednou triedou, zatiaľ bez ošetrovania výnimiek:

class UcetHeslo:
    def __init__(self, meno, heslo, suma=0):
        self.meno, self.heslo, self.suma = meno, heslo, suma

    def __str__(self):
        return f'ucet {self.meno} -> {self.suma} euro'

    def vklad(self, suma):
        if self.heslo == input(f'heslo pre {self.meno}? '):
            self.suma += suma
        else:
            print('chybne heslo')

    def vyber(self, suma):
        if self.heslo == input(f'heslo pre {self.meno}? '):
            if suma <= 0:
                return 0
            suma = min(self.suma, suma)
            self.suma -= suma
            return suma
        else:
            print('chybne heslo')

#---- test ----

mbank = UcetHeslo('mbank', 'heslo1', 100)
csob = UcetHeslo('csob', 'heslo2', 30)
print('*** stav uctov:', mbank, csob, sep='\n')
mbank.vklad(csob.vyber(55))
print('*** stav uctov:', mbank, csob, sep='\n')

Po spustení a zadaní správnych hesiel, dostávame takýto výpis:

*** stav uctov:
ucet mbank -> 100 euro
ucet csob -> 30 euro
heslo pre csob? heslo2
heslo pre mbank? heslo1
*** stav uctov:
ucet mbank -> 130 euro
ucet csob -> 0 euro

Zdá sa, že test prebehol v poriadku.

Prepíšme riešenie s využitím výnimiek. Zadefinujeme pritom vlastnú výnimku ChybnaTransakcia a budeme sa snažiť ošetriť všetky možné chybové situácie:

class ChybnaTransakcia(Exception): pass

class UcetHeslo:
    def __init__(self, meno, heslo, suma=0):
        self.meno, self.heslo, self.suma = meno, heslo, suma

    def __str__(self):
        return f'ucet {self.meno} -> {self.suma} euro'

    def kontrola(self):
        if self.heslo and self.heslo != input(f'heslo pre {self.meno}? '):
            raise ChybnaTransakcia('chybne heslo pre pristup k uctu ' + self.meno)

    def vklad(self, suma):
        self.kontrola()
        try:
            if suma <= 0:
                raise TypeError
            self.suma += suma
        except TypeError:
            raise ChybnaTransakcia('chybne zadana suma pre vklad na ucet ' + self.meno)

    def vyber(self, suma):
        self.kontrola()
        try:
            if suma <= 0:
                raise TypeError
            suma = min(self.suma, suma)
            self.suma -= suma
            return suma
        except TypeError:
            raise ChybnaTransakcia('chybne zadana suma pre vyber z uctu ' + self.meno)

def prevod(ucet1, ucet2, suma):
    '''prevedie sumu z ucet1 na ucet2'''
    suma = ucet1.vyber(suma)
    try:
        ucet2.vklad(suma)
    except ChybnaTransakcia:
        ucet1.suma += suma   # vrati vybratu sumu na ucet1
        raise

#---- test ----

mbank = UcetHeslo('mbank', 'heslo1', 100)
csob = UcetHeslo('csob', 'heslo2', 30)
print('*** stav uctov:', mbank, csob, sep='\n')
prevod(csob, mbank, 55)
print('*** stav uctov:', mbank, csob, sep='\n')

Všimnite si, že pomocná funkcia prevod() sa snaží pri neúspešnej transakcii vrátiť vybratú sumu peňazí - hoci to asi nerobí úplne korektne …


Cvičenia

L.I.S.T.


  1. Do premennej zoz sme priradili dvojprvkový zoznam [1, '2']. Potom sme v príkazom riadku zapisovali pythonovské výrazy, ktoré obsahovali túto premennú zoz. Zakaždým sme dostali nejakú chybovú správu. Pokús sa pre každú túto chybovú správu nájsť pythonovský výraz, ktorý ju vyvolal:

    >>> ...
    IndexError: list index out of range
    >>> ...
    IndexError: pop index out of range
    >>> ...
    ValueError: list.remove(x): x not in list
    >>> ...
    IndexError: list assignment index out of range
    >>> ...
    TypeError: can only concatenate list (not "int") to list
    >>> ...
    TypeError: can't multiply sequence by non-int of type 'list'
    >>> ...
    TypeError: unsupported operand type(s) for /: 'list' and 'int'
    >>> ...
    TypeError: 'list' object is not callable
    >>> ...
    ValueError: 3 is not in list
    >>> ...
    IndexError: pop from empty list
    >>> ...
    TypeError: 'int' object is not iterable
    >>> ...
    TypeError: list indices must be integers or slices, not NoneType
    >>> ...
    TypeError: 'int' object is not callable
    >>> ...
    TypeError: '<' not supported between instances of 'str' and 'int'
    

  1. Napíš funkcie int2(hodnota, nahrada=None) a float2(hodnota, nahrada=None), ktoré prevedú danú hodnotu na celé číslo (funkciou int()), resp. na desatinné (funkciou float()) a ak sa to nedá, funkcie vrátia náhradnú hodnotu. Napríklad:

    >>> print(int2([]))
    None
    >>> int2(3.14, 'abc')
    3
    >>> float2('3.14')
    3.14
    >>> float2('3,14')
    >>> int2('1 2', 999)
    999
    

  1. Napíš funkciu zoznam_cisel(retazec), ktorá sa pokúsi z daného reťazca vytvoriť zoznam celých čísel. Predpokladá, že parametrom je reťazec, v ktorom sú hodnoty oddelené medzerami. Funkcia by nemala spadnúť na chybe. Napríklad:

    >>> zoznam_cisel(None)
    []
    >>> zoznam_cisel('1 2')
    [1, 2]
    >>> zoznam_cisel('1a 2.')
    []
    >>> zoznam_cisel('1a 2 3 4.')
    [2, 3]
    >>> zoznam_cisel([1, 2, 3])    # parametrom nie je retazec
    []
    

  1. Napíš funkciu priemer(zoznam), ktorá vypočíta priemer hodnôt v danom zozname. Využi štandardnú funkciu sum() a to či je zoznam prázdny netestuj príkazom if ale odchyť po vydelení pomocou try - except. Ak je zoznam neprázdny a nedá sa vypočítať súčet prvkov, funkcia priemer() vráti 0, ak je zoznam prázdny, funkcia vráti None. Napríklad:

    >>> priemer([1.5, 2, 3.14, 4])
    2.66
    >>> priemer([1, 2, [3], 4])
    0
    >>> print(priemer([]))
    None
    

  1. Napíš funkciu suma(zoznam), ktorá zistí číselný súčet prvkov zoznamu (bez štandardnej funkcie sum()). Zrejme bude postupne pripočítavať prvky zoznamu a ak sa nejaký pripočítať nedá (neprejde operácia +), tento prvok vynechá. Napríklad:

    >>> suma([1.5, 2, 3.14, 4])
    10.64
    >>> suma([1, 2, [3], 4])
    7
    >>> suma([])
    0
    

  1. Napíš funkciu suma2(postupnost), ktorá zistí číselný súčet prvkov postupnosti (dá sa prechádzať for-cyklom). Ak niektorý z prvkov postupnosti sa pripočítať k výsledku nedá (napr. nie je to číslo), funkcia rekurzívne zavolá samu seba s touto hodnotou a pripočíta jej výsledok. Ak by takéto rekurzívne volanie malo spadnúť na chybe, funkcia to odignoruje. Napríklad:

    >>> suma2([1, 2, '3', 4])
    7
    >>> suma2([1, 2, [3], 4])
    10
    >>> suma2([[], [1], 2, None, (3, 4)])
    10
    >>> suma2([[[[42]]]])
    42
    >>> suma2(((((5.5, '1', 6.5),),),))
    12.0
    

  1. Napíš funkciu pocet_riadkov(meno_suboru), ktorá vráti počet riadkov zadaného súboru. Ak daný súbor neexistuje (nepodaril sa open()), funkcia vráti -1. Napríklad:

    >>> pocet_riadkov('cvicenie.py')
    104
    >>> pocet_riadkov('x.x')    # pre neexistujuci subor
    -1
    

    Nezabudni zatvoriť otvorený súbor.


  1. Napíš funkciu daj_cislo(meno_suboru, index, nahrada=0), ktorá predpokladá, že daný súbor má v každom riadku po jednom celom čísle. Funkcia vráti číselnú hodnotu z tohto riadka a ak sa to nedá (napr. súbor neexistuje, nemá dosť riadkov, nie je v tomto riadku iba jediná celočíselná hodnota), vráti hodnotu tretieho parametra nahrada. Napríklad:

    >>> daj_cislo('cvicenie.py', 17, 'neviem')
    'neviem'
    

    Nezabudni zatvoriť otvorený súbor.


  1. Napíš funkciu sustavy(retazec), ktorá sa pokúsi daný reťazec previesť na číslo v rôznych číselných sústavách. Využi to, že štandardná funkcia int() môže byť zavolaná aj s druhým parametrom - číselnou sústavou, napr. int('ff', 16) vráti 255, t.j. 'ff' je v 16-ovej sústave číslo 255. Funkcia sustavy() vráti 17-prvkový zoznam, pričom i-ty prvok zoznamu obsahuje prevod daného reťazca na číslo v i-sústave. Ak sa to pre nejakú sústavu urobiť nedá, prvok zoznamu na danom mieste bude mať hodnotu None. Otestuj štandardnú funkciu int, napríklad:

    >>> int('1101')
    1101
    >>> int('1101', 2)      # dvojkova sustava
    13
    >>> int('1101', 3)
    37
    >>> int('1101', 16)
    4353
    >>> int('3a')
    ...
    ValueError: invalid literal for int() with base 10: '3a'
    >>> int('3a', 11)
    43
    >>> int('3a', 16)
    58
    

    Napríklad:

    >>> sustavy('11')
    [11, None, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
    >>> sustavy('1a1')
    [None, None, None, None, None, None, None, None, None, None, None, 232, 265, 300, 337, 376, 417]
    >>> sustavy('FF')
    [None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, 255]
    >>> sustavy('x')
    [None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
    

  1. Napíš funkciu ako(hodnota1, hodnota2), ktorá najprv zistí typ prvého parametra hodnota1 a potom sa pokúsi pretypovať na tento typ druhý parameter hodnota2 (zrejme volaním typ(hodnota2), kde typ je typom prvej hodnoty). Ak sa toto pretypovanie úspešne podarí, funkcia vráti túto pretypovanú hodnotu, inak vráti None. Vyskúšaj:

    >>> typ = type('abc')
    >>> typ([1, 2, 3])
    '[1, 2, 3]'
    >>> typ(1+2)
    '3'
    >>> typ
    <class 'str'>
    >>> typ = type([1, 'x'])
    >>> typ('ahoj')
    ['a', 'h', 'o', 'j']
    >>> typ(123)              # nefunguje list(123)
    ...
    TypeError: 'int' object is not iterable
    >>> typ
    <class 'list'>
    >>> type(37)(3.14)
    3
    >>> type('37')(3.14)
    '3.14'
    

    Napríklad:

    >>> ako('a', 123)
    '123'
    >>> ako(3.1, 123)
    123.0
    >>> ako((), '123')
    ('1', '2', '3')
    >>> print(ako((), 123))
    None
    

  1. Napíš funkciu sucet(zoznam), ktorá spočíta všetky prvky daného zoznamu. Pracovať bude tak, že najprv zoberie prvý prvok zoznamu a ten priradí do vysl ako momentálny výsledok - k nemu budete postupne pripočítavať (použije operáciu +) ďalšie prvky zoznamu. Lenže tieto ďalšie prvky v zozname nemusia byť rovnakého typu ako prvý prvok a preto ich bude postupne pretypovávať na rovnaký typ a až pri úspešnom pretypovaní ich pripočíta k výsledku vysl, inak ich odignoruje. Napríklad:

    >>> sucet([1, 2, 3.1, '4'])
    10
    >>> sucet([1., 2, 3.1, '4'])
    10.1
    >>> sucet([('p', 'y'), 'tho', ['n']])
    ('p', 'y', 't', 'h', 'o', 'n')
    >>> sucet([[1], (2, 3), 4])
    [1, 2, 3]
    

  1. Zadefinuj metódy triedy TelefonnyZoznam podobnej z 15. cvičenia:

    class TelefonnyZoznam:
        def __init__(self, meno_suboru=None):
            ...
    
        def pridaj(self, meno, telefon):
            ...
    
        def zisti(self, meno):
            ...
    
        def citaj(self, meno_suboru):
            ...
    
        def zapis(self, meno_suboru):
             ...
    
         def vypis(self):
             ...
    

    Metódy:

    • __init__(meno_suboru=None) inicializuje self.zoznam a zavolá self.citaj(meno_suboru), prípadnú výnimku bude teraz ignorovať

    • pridaj(meno, telefon) pridá dvojicu (tuple) do zoznamu alebo nahradí, ak už také meno bolo v zozname

      • ak meno alebo telefon nie je reťazec, vyvolá výnimku TypeError

    • zisti(meno) vráti príslušné telefónne číslo alebo vyvolá výnimku KeyError

    • citaj(meno_suboru) prečíta obsah súboru (v každom riadku dva reťazce oddelené medzerou), pri chybe vyvolá ValueError

    • zapis(meno_suboru) do každého riadka zapíše meno a telefón, oddelí medzerou, prípadnú chybu pri zápise vráti ako ValueError

    • vypis() vypíše do textovej plochy


7. Domáce zadanie

L.I.S.T.


Napíš pythonovský modul, ktorý bude obsahovať jedinú triedu Pajton a žiadne iné globálne premenné, funkcie a triedy:

class Pajton:
    def __init__(self):
        self.tab = []

    def prem(self, meno):
        return ...

    def vyraz(self, retazec):
        return ...

    def prirad(self, meno, hodnota):
        ...

    def prikaz(self, retazec):
        if retazec == '?':
            self.vypis()
            return
        if retazec == 'dir()':
            return self.dir()
        ...

    def dir(self):
        return ...

    def vypis(self):
        for meno, hodnota in self.tab:
            print(meno, '=', hodnota)

Táto trieda umožní programovať vo veľmi jednoduchom programovacom jazyku. V tomto jazyku môžeme postupne zadávať priraďovacie príkazy alebo zisťovať hodnoty výrazov podobne ako v IDLE jazyka Python. Tento zjednodušený programovací jazyk zvláda len celočíselnú aritmetiku s operáciami +, -, *, /, pričom / označuje celočíselné delenie (pythonovské //). Aritmetické výrazy môžu byť len týchto dvoch typov:

  • jednoduchý operand:

    • celé číslo

    • meno premennej (ak ešte nemala priradenú hodnotu, bude to chyba NameError)

  • operand operácia operand, kde operácia je jeden zo symbolov +, -, *, /

    • medzi operandmi a operáciou musí byť aspoň jedna medzera, inak je to SyntaxError

Priraďovací príkaz má tvar:

  • premenná = výraz

    • znak = musí byť oddelený od premennej a výrazu aspoň jednou medzerou, inak je to SyntaxError

Ako príkaz môžeme tomuto jazyku zadať:

  • aritmetický výraz - vtedy vypíše jeho hodnotu

  • priraďovací príkaz - vtedy nič nevypisuje len vykoná priradenie

  • znak ? - vtedy vypíše hodnoty všetkých premenných, do ktorých sa niečo doteraz priradilo

  • príkaz dir() - vtedy vypíše zoznam všetkých mien premenných

Metódy majú fungovať takto:

  • inicializácia __init__() vytvorí prázdnu tabuľku premenných, bude to zoznam (list), ktorý bude obsahovať dvojice (tuple) mien a zodpovedajúcich hodnôt, nemeňte meno atribútu tab, lebo testovač bude kontrolovať jeho obsah

  • funkcia prem(meno) vráti hodnotu zodpovedajúcu premennej s daným menom, ak takéto meno v tabuľke self.tab nenájde, vyvolá výnimku NameError

  • funkcia vyraz(retazec) vráti hodnotu aritmetického výrazu (hodnotou bude celé číslo), alebo vyvolá jednu z chýb NameError alebo SyntaxError

  • metóda prirad(meno, hodnota) skontroluje korektnosť mena premennej (či nezačína číslicou a obsahuje len písmená, číslice resp. podčiarkovníky) a v tabuľke self.tab si pre dané meno zapamätá novú hodnotu

  • metóda prikaz(retazec), ak je reťazec korektný, zavolá metódy vyraz alebo prirad a vráti buď hodnotu výrazu (celé číslo) alebo vykoná priradenie a vtedy vráti None; okrem týchto prípadov, metóda môže vyvolať aj metódy vypis a dir

  • funkcia dir() vráti zoznam mien všetkých premenných

  • metóda vypis() je už naprogramovaná, nemusíš ju meniť

Na testovanie môžeš využiť tento kód (umiestni ho za definíciu triedy, môže ostať v module, aj keď ho budeš odovzdávať na testovanie):

if __name__ == '__main__':
    p = Pajton()
    while True:
        try:
            hodn = p.prikaz(input('>>> '))
            if hodn is not None:
                print(hodn)
        except SyntaxError:
            print('+++ syntakticka chyba +++')
        except NameError:
            print('+++ chybne meno premennej +++')

Nemeň mená metód a parametrov.

Tvoj odovzdaný program s menom riesenie.py musí začínať tromi riadkami komentárov:

# 7. zadanie: pajton
# autor: Janko Hraško
# datum: 28.11.2019

Projekt riesenie.py odovzdávaj na úlohový server https://list.fmph.uniba.sk/ najneskôr do 23:00 6. decembra, kde ho môžeš nechať otestovať. Testovač bude spúšťať metódy s rôznymi vstupmi. Odovzdať projekt aj ho testovať môžeš ľubovoľný počet krát. Môžeš zaň získať 10 bodov.