Cvičení č. 17 rozšířené o poznámky ze cvičení a řešení některých příkladů (ZS 2024/25)¶

V minulém díle jste viděli...¶

Generátory¶

Generátory jsou zvláštním druhem iterátoru, který iteruje přes průběžně generované hodnoty, které generuje. generátorové funkce (generator function) nebo generátorového výrazu (generator expression).

Generátorová funkce je taková funkce, která hodnoty nevrací (pomocí příkazu return), ale generuje (pomocí příkazu yield).

In [2]:
def generate_squares(n):
    for x in range(n):
        yield x ** 2

Kdykoliv v programu zavoláme generátorovou funkci, funkce vrátí nový generátor.

In [3]:
m = generate_squares(5)
print(m)

for x in m:
    print(x, end = " ")
<generator object generate_squares at 0x0000021D2E6EBD60>
0 1 4 9 16 

Generátorové výrazy umožňují vytvářet generátory bez nutnosti vytváření generátorových funkcí. Generátorová notace (comprehension) u modifikovatelných (mutable) kontejnerů nám pak výrazně zjednodušuje vytváření a zpracování těchto objektů.

Základní syntaxe je následující:

In [ ]:
new_generator = (expression for member in iterable)
new_list = [expression for member in iterable]
new_set  = {expression for member in iterable}
new_dict = {key:expression for member in iterable}

Například pro seznam můžeme příkaz rozepsat jako:

In [ ]:
new_list = []
for element in iterable:
    my_list.append(expression(element))

Generové výrazy můžeme rozšířit o podmínky a můžeme pomocí nich kombinovat hodnoty z více zdrojů:

  • Filtrování (obdoba funkce filter):
In [ ]:
new_generator = (expression for member in iterable if condition)
  • Kombinace hodnot z více zdrojů:
In [ ]:
 new_generator = (expression for member1 in iterable1 (if condition1)
             for member2 in iterable2 (if condition2) 
             ...
             for memberN in iterableN (if conditionN))

Zpracování chyb, výjimky, aserce¶

Neoddělitelnou součástí programování je také práce s chybami. Praxe ukazuje, že téměř každý alespoň trošku složitý program obsahuje nějakou chybu. Chyby obvykle dělíme přinejmenším do dvou skupin: syntaktické chyby a běhové chyby.

Syntaktické chyby¶

Syntaktické chyby (anglicky syntax errors) představují porušení pravidel zápisu jazyka v daném programovacím jazyce. Na syntaktickou chybu upozorní interpret (případně překladač) a označí i řádek ve zdrojovém kódu, kde se chyba nachází. Dokud programátor neopraví všechny syntaktické chyby, není možné program interpretovat (případně přeložit a spustit). Výhodou tedy je, že se o syntaktické chybě nikdo kromě samotného programátora nedozví. Moderní vývojová prostředí umí syntaktické chyby zvýrazňovat přímo při psaní zdrojového kódu.

Následující ukázka obsahuje syntaktickou chybu:

In [1]:
print "Hello world"
  Cell In[1], line 1
    print "Hello world"
    ^
SyntaxError: Missing parentheses in call to 'print'. Did you mean print(...)?

Interpret označí, že na řádce 1 našel chybu Missing parentheses in call to 'print', která značí, že ve volání funkce print chybí závorky. V tomto případě interpret také nabídne možnost opravy.

Po opravě už bude možné program spustit:

In [5]:
print("Hello world")
Hello world
In [6]:
# chyba v odsazení
print("Hello world")
  print("Hello world")
  Cell In[6], line 2
    print("Hello world")
    ^
IndentationError: unexpected indent

Běhové chyby¶

Běhové chyby (anglicky runtime errors) se projeví až při běhu programu a většinou vedou k jeho havárii (pádu). Mezi běhové chyby patří dělení nulou, použití ještě nevytvořeného objektu, použití objektu špatného datového typu, a podobně. V následující ukázce dojde k běhové chybě:

In [7]:
def faktorial(n):
    # f = 1
    while n > 0:
        f = f * n
        n = n -1
    return f

print(faktorial(5))
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
Cell In[7], line 8
      5         n = n -1
      6     return f
----> 8 print(faktorial(5))

Cell In[7], line 4, in faktorial(n)
      1 def faktorial(n):
      2     #f = 1
      3     while n > 0:
----> 4         f = f * n
      5         n = n -1
      6     return f

UnboundLocalError: cannot access local variable 'f' where it is not associated with a value

Po spuštění skriptu dojde k pádu programu a zobrazí se hlášení, že k lokální proměnné f bylo přistoupeno před její inicializací. Řešením tohoto problému je definovat proměnnou f na začátku funkce faktorial a přiřadit jí hodnotu 1.

Poznámka: Některé prameny uvádějí ještě třetí typ chyb: logické (nebo sémantické). Tyto chyby bývá největší problém odhalit, protože navenek program běží, ale počítá něco špatně.

Výjimky jako reakce na vznik běhových chyb¶

Protože je programovací jazyk Python silně zaměřen na objektově orientované programování, nepřekvapí, že se v něm s chybami zachází jako s objekty. Protože by k chybám v dobře napsaném programu mělo docházet pouze výjimečně, zažil se pro tyto objekty pojem výjimky (anglicky exceptions). Takový objekt pak nese informace o chybě:

  • na jakém místě v programu k chybě došlo
  • jak se program na místo vzniku chyby dostal
  • jaký typ chyby nastal

V situaci, kdy je v programu splněna podmínka vedoucí k běhové chybě, existuje možnost vyvolat výjimku (anglicky raise exception). Ve chvíli vyvolání výjimky je provádění aktuální funkce přerušeno, a v hierarchii volání se hledá kód, který by mohl danou výjimku zachytit (anglicky catch) a ošetřit (anglicky handle). V případě, že není nalezen žádný takový kód (často označovaný jako handler), program zhavaruje (spadne, crashne) a zobrazí chybové hlášení.

Na jednotlivé kroky se nyní podíváme podrobněji.

Vyvolání výjimky¶

Pro vyvolání výjimky se v jazyce Python používá příkaz raise, ke kterému je možné přidat informaci o typu chyby a její textový popis:

In [9]:
def hustota(m, v):
    if v == 0:
        raise ZeroDivisionError("Nepodělíš nulou!")
    return m/v

hustota(5.0, 0.0)
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[9], line 6
      3         raise ZeroDivisionError("Nepodělíš nulou!")
      4     return m/v
----> 6 hustota(5.0, 0.0)

Cell In[9], line 3, in hustota(m, v)
      1 def hustota(m, v):
      2     if v == 0:
----> 3         raise ZeroDivisionError("Nepodělíš nulou!")
      4     return m/v

ZeroDivisionError: Nepodělíš nulou!

Vzhledem k tomu, že vyvolanou výjimku v kódu nikde nezachytáváme, dojde k vypsání informací o vzniklé chybě a následně k předčasnému ukončení programu. Součástí chybové zprávy budou i informace o vzniklé chybě získané z chybového objektu.

Poznámka: V této ukázce používáme třídu Exception, která je předkem všech standardních výjimek jazyka Python. Poznamenejme, že jednou z odvozených tříd je i výjimka ZeroDivisionError, která by pro tento příklad byla vhodnější. Přehled nejčastějších standardních výjimek bude uveden později.

Výjimka typu AssertionError¶

Při ladění a testování zdrojového kódu můžeme v jazyce Python použít klíčové slovo assert, které slouží k ověřování platnosti podmínek:

In [8]:
def faktorial(n):
    assert n >= 0, "Nelze počítat faktorial ze záporného čísla"
    f = 1
    for x in range(2, n + 1):
        f = f * x
    return f

print(faktorial(5))
120

V případě, že je uvedená podmínka splněna, volání assert nic nezpůsobí. Naopak, pokud podmínka splněna nebude, vyvolá se standardní výjimka typu AssertionError a program se ukončí.

In [11]:
print(faktorial(-1))
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[11], line 1
----> 1 print(faktorial(-1))

Cell In[10], line 2, in faktorial(n)
      1 def faktorial(n):
----> 2     assert n >= 0, "Nelze počítat faktorial ze záporného čísla"
      3     f = 1
      4     for x in range(2, n + 1):

AssertionError: Nelze počítat faktorial ze záporného čísla

Použití klíčového slova assert nám tedy umožní program odladit tak, aby byly všechny požadované podmínky splněny.

Poznámka: Dodejme, že klíčové slovo assert byste již měli znát z domácích úkolů, kde se používá ve veřejných i skrytých testech.

Nyní se podíváme na způsob, jakým se výjimky zachytávají a ošetřují.

Ošetřování výjimek¶

K zachycení a ošetření výjimek v jazyce Python nám slouží pokusný blok uvozený klíčovým slovem try doplněný o jeden nebo více bloků except. V případě, že některý příkaz v pokusném bloku vyvolá výjimku, přeruší se vykonávání tohoto bloku a řízení se přesune do bloku except. Bloku except se také říká handler, a má na starost ošetření výjimek vzniklých v předchozím pokusném bloku. Pokud v pokusném bloku žádný příkaz výjimku nevyvolá, blok except se vynechá.

In [12]:
try:
    print(faktorial(5))
    print(faktorial(-5))
    print(faktorial(20))
except:
    pass

print("Toto je první příkaz za handlerem")
120
Toto je první příkaz za handlerem

Při výpočtu faktoriálu hodnoty -5 nebyla ve funkci faktorial splněna podmínka kladnosti parametru, což způsobilo vyvolání standardní výjimky (typu AssertionError) a dále předčasné ukončení fukce faktorial a také zbytku pokusného bloku. Řízení pak pokračovalo v bloku except, který by měl zachycenou výjimku ošetřit.

Poznámka: V bloku except jsme použili prázdný příkaz pass, který nic nedělá, ale je možné ho z hlediska syntaxe použít na jakémkoliv místě, kde je očekáván nějaký příkaz (např. tělo funkce, tělo cyklů a podmínek). Používá se při psaní programu na vyplnění ještě neimplementovaných pasáží zdrojového kódu.

Upravme předchozí ukázku tak, aby handler vykonal nějakou činnost. K tomu, abychom zjistili informaci o chybě, musíme zachytit vyvolanou výjimku (v našem případě typu AssertionError):

In [14]:
try:
    print(faktorial(5))
    print(faktorial(-5))
    print(faktorial(20))
except AssertionError as error:
    print("Zachytil jsem výjimku typu AssertionError. Došlo k následující chybě:")
    print(error)

print("Toto je první příkaz za handlerem")
120
Zachytil jsem výjimku typu AssertionError. Došlo k následující chybě:
Nelze počítat faktorial ze záporného čísla
Toto je první příkaz za handlerem

Všimněte si, že v tomto případě je za klíčovým slovem except uveden typ výjimky, kterou daný handler obslouží. Za klíčovým slovem as se píše název, pod kterým budeme se zachycenou výjimkou pracovat. Můžeme ji třeba předat funkci print, která vypíše textový popis chyby.

Za blokem try může být handlerů více, každý z nich pak obslouží jiný typ výjimky:

In [15]:
try:
    print(faktorial(5))
    print(faktorial("10"))
    print(faktorial(20))
except AssertionError as error:
    print("Zachytil jsem výjimku typu AssertionError. Došlo k následující chybě:")
    print(error)
except TypeError as type_error:
    print("Zachytil jsem výjimku typu TypeError. Došlo k následující chybě:")
    print(type_error)
    
print("Toto je první příkaz za handlerem")
120
Zachytil jsem výjimku typu TypeError. Došlo k následující chybě:
'>=' not supported between instances of 'str' and 'int'
Toto je první příkaz za handlerem

V tomto případě tedy první handler obsluhuje výjimky typu AssertionError a druhý pak TypeError. Při práci s výjimkami je potřeba doplnit handler pro každý typ výjimky, který v pokusném bloku může vzniknout.

Univerzální handler umí obsloužit všechny typy výjimek odvozené od třídy Exception. Je uvozen pomocí except: nebo except Exception ... :

In [9]:
try:
    print(faktorial(5))
    1/0
    print(faktorial("10"))
    print(faktorial(20))
except AssertionError as error:
    print("Zachytil jsem výjimku typu AssertionError. Došlo k následující chybě:")
    print(error)
except TypeError as type_error:
    print("Zachytil jsem výjimku typu TypeError. Došlo k následující chybě:")
    print(type_error)
except Exception as error:
    print("Zachytil jsem nějakou nečekanou chybu:")
    print(error)
    
print("Toto je první příkaz za handlerem")
120
Zachytil jsem nějakou nečekanou chybu:
division by zero
Toto je první příkaz za handlerem
In [10]:
# na pořadí handlerů záleží:
try:
    print(faktorial(5))
    print(faktorial("10"))
    print(faktorial(20))
except Exception as error:
    print("Zachytil jsem nějakou nečekanou chybu:")
    print(error)
except AssertionError as error:
    print("Zachytil jsem výjimku typu AssertionError. Došlo k následující chybě:")
    print(error)
except TypeError as type_error:
    print("Zachytil jsem výjimku typu TypeError. Došlo k následující chybě:")
    print(type_error)

    
print("Toto je první příkaz za handlerem")
120
Zachytil jsem nějakou nečekanou chybu:
'>=' not supported between instances of 'str' and 'int'
Toto je první příkaz za handlerem

Varování: Použití univerzálního handleru je jeden z nejnebezpečnějších zlozvyků (antipattern). Kód

try:
    něco_udělej()
except:
    pass
sice zachytí všechny výjimky a tím zabrání okamžitému pádu programu, ale na druhou stranu všechny chyby, i ty neočekávané, v tichosti zamaskuje! Tímto způsobem pak často projdou chyby nepozorovaně do produkční verze programu.

Poznámka: V jazyce Python se používá styl psaní kódu označovaný zkratkou EAFP - easier to ask for forgiveness than permission neboli raději prosit o odpuštění než o povolení. To znamená, že se při práci se slovníky a objekty předpokládá existence klíčů a atributů a v případě jejich neexistence se zachytí výjimka. Součástí kódu tak bude řada pokusných bloků doplněných o příslušné handlery.

Na druhou stranu v jiných programovacích jazycích, jako třeba C, dáváme přednost principu LBYL - look before you leap neboli rozhlédni se před tím, než skočíš. V tomto případě existenci klíčů a atributů ověřuje před tím, než k nim přistoupíme. Součástí kódu tak bude řada podmínek. Tento přístup může působit problémy ve vícevláknovém programování. Například, následující ukázka:

if key in dictionary:
    return dictionary[key]
selže, pokud jiné vlákno odstraní klíč ze slovníku po testu, ale před přístupem k hodnotě. Řešením je použít zamykání nebo EAFP přístup.

In [23]:
# chyby nejsou ošetřené:
s = [1, 2, 3]
i = int(input("zadej index"))
print(s[i])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[23], line 2
      1 s = [1, 2, 3]
----> 2 i = int(input("zadej index"))
      3 print(s[i])

ValueError: invalid literal for int() with base 10: 'ahoj'
In [31]:
# princip LBYL:
s = [1, 2, 3]
i_s = input("zadej index")
if not i_s.isdigit(): 
    print("Index musí být celé číslo. ")
else:
    i = int(i_s)
    if i >= len(s) or i < -len(s):
        print("Index je mimo rozsah. ")
    else:
        print(s[i])
Index musí být celé číslo. 
In [11]:
# princip EAFP:
s = [1, 2, 3]
try:
    i = int(input("zadej index"))
    print(s[i])
except IndexError as e:
    print("Index je mimo rozsah. ", e)
except ValueError as e:
    print("Index musí být celé číslo. ", e)
Index musí být celé číslo.  invalid literal for int() with base 10: 'ahoj'
In [12]:
# řízení cyklu pomocí výjimek:
s = [1, 2, 3]
while True:
    try:
        i = int(input("zadej index"))
        print(s[i])
        break
    except IndexError as e:
        print("Index je mimo rozsah. ", e)
    except ValueError as e:
        print("Index musí být celé číslo. ", e)
Index musí být celé číslo.  invalid literal for int() with base 10: 'ahoj'
Index je mimo rozsah.  list index out of range
1

V následující tabulce jsou vypsány časté výjimky, se kterými se můžete při programování potkat:

Výjimka Krátký popis
ValueError vzniká při pokusu předat do funkce neplatnou hodnotu argumentu
TypeError vzniká při pokusu o provedení operace s objektem nevhodného typu (např. operátor dělení pro typ str)
NameError vzniká při pokusu o přístup k funkci nebo proměnné, jejichž jméno není nalezeno v aktuální oblasti viditelnosti
IndentationError vzniká v případě chybně odsazeného kódu
RecursionError vzniká v případě překročení maximální hloubky rekurze
ZeroDivisionError vzniká při pokusu o dělení nulou
AssertionError vzniká, pokud není splněna podmínka v příkazu assert
IndexError vzniká při indexování posloupností, pokud je index mimo meze
KeyError vzniká, pokud přistupujeme k neexistujícímu klíči slovníku
StopIteration vzniká, pokud se pokusíme použít iterátor, který už je na konci
IOError vzniká v případě chyby při vstupně-výstupních operacích (práce se soubory)
ImportError vzniká, pokud příkaz import nenalezne požadovaný modul

Podrobnější popis a kompletní tabulku standardních výjimek je možné nalézt v referenční dokumentaci jazyka Python.

V jednom handleru můžeme zachytit i více typů výjimek, pokud je zapíšeme jako n-tici. V ukázce je použití funkce type(var), která vrací objekt obsahující informaci o typu proměnné var. Jeho atribut __name__ pak obsahuje název typu.

In [15]:
# pripomenuti: funkce type
type("")  == str
Out[15]:
True
In [16]:
type("")
Out[16]:
str
In [13]:
# nazev typu jako string:
type("").__name__
Out[13]:
'str'
In [34]:
typeE = None
try:
    #print(faktorial(-5))
    print(faktorial("10"))
    print(faktorial(20))
except (AssertionError, TypeError) as error:
    error_name = type(error).__name__
    print(f"Zachytil jsem výjimku typu {error_name}. Došlo k následující chybě:")
    print(error)

print("Toto je první příkaz za handlerem")
Zachytil jsem výjimku typu TypeError. Došlo k následující chybě:
'>=' not supported between instances of 'str' and 'int'
Toto je první příkaz za handlerem

Klauzule else a finally¶

Za handlery může volitelně následovat blok else: s příkazy, které mají být vykonány v případě, že v pokusném bloku nenastala chyba. Na úplný závěr může být blok finally:, který se vykoná vždy, bez ohledu na to, zda v pokusném bloku byla vyvolána výjimka, či nikoliv:

In [22]:
# případ bez chyby:
try:
    f = faktorial(5)
except (AssertionError, TypeError) as error:
    error_name = type(error).__name__
    print(f"Zachytil jsem výjimku typu {error_name}. Došlo k následující chybě:")
    print(error)
else:
    print("Pokusný blok proběhl bez chyb, výsledek je", f)
finally:
    print("Tento blok se provede vždy")
Pokusný blok proběhl bez chyb, výsledek je 120
Tento blok se provede vždy
In [23]:
# případ s chybou:
try:
    f = faktorial(-5)
except (AssertionError, TypeError) as error:
    error_name = type(error).__name__
    print(f"Zachytil jsem výjimku typu {error_name}. Došlo k následující chybě:")
    print(error)
else:
    print("Pokusný blok proběhl bez chyb, výsledek je", f)
finally:
    print("Tento blok se provede vždy")
Zachytil jsem výjimku typu AssertionError. Došlo k následující chybě:
Nelze počítat faktorial ze záporného čísla
Tento blok se provede vždy
In [21]:
# případ s chybou a raise:
try:
    f = faktorial(-5)
except (AssertionError, TypeError) as error:
    error_name = type(error).__name__
    #raise ZeroDivisionError("Test")
    print(f"Zachytil jsem výjimku typu {error_name}. Došlo k následující chybě:")
    print(error)
    raise
else:
    print("Pokusný blok proběhl bez chyb, výsledek je", f)
finally:
    print("Tento blok se provede vždy")
Zachytil jsem výjimku typu AssertionError. Došlo k následující chybě:
Nelze počítat faktorial ze záporného čísla
Tento blok se provede vždy
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[21], line 3
      1 # případ s chybou a raise:
      2 try:
----> 3     f = faktorial(-5)
      4 except (AssertionError, TypeError) as error:
      5     error_name = type(error).__name__

Cell In[8], line 2, in faktorial(n)
      1 def faktorial(n):
----> 2     assert n >= 0, "Nelze počítat faktorial ze záporného čísla"
      3     f = 1
      4     for x in range(2, n + 1):

AssertionError: Nelze počítat faktorial ze záporného čísla

Poznánka: Při obsluze výjimky může v rámci handleru vzniknout další výjimka. Tuto sekundární výjimku ovšem neobslouží ostatní handlery na této úrovni, protože nejsou uvnitř pokusného bloku. Nicméně blok finally se provede i v tomto případě. V následující ukázce si všimněte hlášení During handling of the above exception, another exception occurred (při obsluze předchozí výjimky nastala další výjimka) a dále výpisu z finally bloku:

In [24]:
# případ s chybou a vyvoláním jiné chyby v handleru:
try:
    f = faktorial("5")
except (AssertionError, TypeError) as error:
    error_name = type(error).__name__
    raise ZeroDivisionError("Test")
    print(f"Zachytil jsem výjimku typu {error_name}. Došlo k následující chybě:")
    print(error)
else:
    print("Pokusný blok proběhl bez chyb, výsledek je", f)
finally:
    print("Tento blok se provede vždy")
Tento blok se provede vždy
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[24], line 3
      2 try:
----> 3     f = faktorial("5")
      4 except (AssertionError, TypeError) as error:

Cell In[8], line 2, in faktorial(n)
      1 def faktorial(n):
----> 2     assert n >= 0, "Nelze počítat faktorial ze záporného čísla"
      3     f = 1

TypeError: '>=' not supported between instances of 'str' and 'int'

During handling of the above exception, another exception occurred:

ZeroDivisionError                         Traceback (most recent call last)
Cell In[24], line 6
      4 except (AssertionError, TypeError) as error:
      5     error_name = type(error).__name__
----> 6     raise ZeroDivisionError("Test")
      7     print(f"Zachytil jsem výjimku typu {error_name}. Došlo k následující chybě:")
      8     print(error)

ZeroDivisionError: Test

Vyvolání a šíření výjimky¶

Nyní už byste měli vědět, jak výjimku zachytit a ošetřit, zbývá ukázat, jak výjimku vyvolat a jakým způsobem se v programu šíří. K vyvolání výjimky slouží příkaz raise doplněný o typ výjimky, která se má vyvolat. Typem výjimky se rozumí konkrétní objekt - třída odvozená od předka Exception.

V následující ukázce funkce fukce_volana() vyvolá výjimku typu ValueError, což způsobí začátek šíření výjimky. Během něho se hledá handler odpovídajícího typu, který by výjimku zvládl ošetřit. Pokud se handler nenajde ve volané funkci, dojde k jejímu předčasnému ukončení a handler se dále hledá ve volající funkci. V našem případě se výjimka po jednotlivých patrech stromu volání rozšířila až na globální úroveň, na které se konečně našel příslušný handler. Pokud by se nenašel ani na globální úrovni, program by se ukončil (neošetřená výjimka). Všimněte si, že v tomto handleru voláme znovu příkaz raise - tím umožníme další šíření výjimky.

In [42]:
def funkce_volana():
    raise ValueError("Ahoj, já jsem výjimka typu ValueError")

def funkce_volajici():
    funkce_volana()
#funkce_volajici()
try:
    funkce_volajici()
except ValueError as error:
    print(error)
    raise
Ahoj, já jsem výjimka typu ValueError
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[42], line 8
      6 #funkce_volajici()
      7 try:
----> 8     funkce_volajici()
      9 except ValueError as error:
     10     print(error)

Cell In[42], line 5, in funkce_volajici()
      4 def funkce_volajici():
----> 5     funkce_volana()

Cell In[42], line 2, in funkce_volana()
      1 def funkce_volana():
----> 2     raise ValueError("Ahoj, já jsem výjimka typu ValueError")

ValueError: Ahoj, já jsem výjimka typu ValueError

Program nakonec opravdu zhavaruje, protože druhé vyvolání výjimky už není nikde zachyceno. Všimněte si, že v chybovém výpisu je zobrazen graf volání (anglicky traceback nebo taky stack trace)

Výhody a nevýhody práce s výjimkami¶

Použití výjimek v programu přináší řadu výhod:

  • vyšší spolehlivost: použití výjimek umožní detekovat nečekané chyby
  • čistější kód a jednodušší obsluha chyb: použití výjimek umožní oddělit kód pro ošetřování chyb od výpočetní logiky
  • snadnější ladění: výpis informací, které výjimka nese usnadní identifikaci problému a výpis grafu volání umožní i vystopovat, kde k chybě došlo

Nicméně, s výjimkami jsou spjaty i některé nevýhody:

  • dopad na výkon: s obsluhou výjimek jsem spojena režie, která snižuje výkon programu
  • zvýšení složitosti kódu: zvláště pokud pracujeme s mnoha typy výjimek, může dojít ke zvýšení složitosti zdrojových souborů související s přidanou logikou pro obsluh chyb
  • potenciální zvýšení zranitelnosti: špatně obsloužená výjimka může zveřejnit citlivé informace a vytvořit bezpečnostní zranitelnost

I přes tyto nedostatky zůstávají výjimky základním nástrojem pro řešení běhových chyb v jazyce Python.

Příklady¶

1. Základy práce s výjimkami¶

Zkuste si pro každý z následujících typů chyb napsat chybný kód, který ji vyvolá.

Výjimka Krátký popis
ZeroDivisionError vzniká při pokusu o dělení nulou
IndexError vzniká při indexování posloupností, pokud je index mimo meze
KeyError vzniká u slovníku, pokud přistupujeme k neexistujícímu klíči
StopIteration vzniká, pokud se pokusíme použít iterátor, který už je na koci
NameError vzniká při pokusu o přístup k funkci nebo proměnné, jejichž jméno není nalezeno v aktuální oblasti viditelnosti
ValueError vzniká při pokus předat do funkce neplatnou hodnotu argumentu
TypeError vzniká, když operátor nebo funkci aplikujeme na objekt nesprávného typu
In [43]:
# ZeroDivisionError:
1 / 0
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[43], line 2
      1 # ZeroDivisionError:
----> 2 1 / 0

ZeroDivisionError: division by zero
In [44]:
# ZeroDivisionError:
raise ZeroDivisionError("delis nulou")
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[44], line 1
----> 1 raise ZeroDivisionError("delis nulou")

ZeroDivisionError: delis nulou
In [25]:
# IndexError:
s = [1]
s[4]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[25], line 3
      1 # IndexError:
      2 s = [1]
----> 3 s[4]

IndexError: list index out of range
In [26]:
# KeyError:
d = {"a":"b"}
d["q"]
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[26], line 3
      1 # KeyError:
      2 d = {"a":"b"}
----> 3 d["q"]

KeyError: 'q'
In [47]:
# StopIteration:
s = [1, 2]
it = iter(s)
next(it)
next(it)
next(it)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
Cell In[47], line 6
      4 next(it)
      5 next(it)
----> 6 next(it)

StopIteration: 
In [27]:
# NameError:
abcdef + 3
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[27], line 2
      1 # NameError:
----> 2 abcdef + 3

NameError: name 'abcdef' is not defined
In [48]:
# ValueError:
int("ahoj")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[48], line 2
      1 # ValueError:
----> 2 int("ahoj")

ValueError: invalid literal for int() with base 10: 'ahoj'
In [52]:
# ValueError:
import math 
math.log(-8)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[52], line 2
      1 import math 
----> 2 math.log(-8)

ValueError: math domain error
In [28]:
# TypeError:
"abc" + 5
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[28], line 2
      1 # TypeError:
----> 2 "abc" + 5

TypeError: can only concatenate str (not "int") to str

Jednu z chyb vložte do pokusného bloku a odchytněte ji pomocí vhodného (dostatečně konkrétního) handleru. Vypiště textový popis chyby.

In [29]:
try:
    "abc" + 5
except TypeError as e:
    print(type(e).__name__, ":",  e)
TypeError : can only concatenate str (not "int") to str

2. Dělitelnost¶

Napište funkci je_dělitelné(n, d), která vrací True nebo False podle toho, jestli je zadané přirozené číslo n dělitelné číslem přirozeným číslem d nebo ne. Pokud n nebo d není typu int nebo se nejedná o kladné číslo, funkce vyvolá vhodný typ výjimky (TypeError nebo ValueError) s vhodným textovým popisem chyby.

In [53]:
isinstance(6, int)
Out[53]:
True
In [54]:
type(6) == int
Out[54]:
True
In [35]:
def je_dělitelné(n, d):
    if not isinstance(n, int) or type(d) != int:
        raise TypeError("Argumenty musí být typu int.")
    if n <= 0 or d <= 0:
        raise ValueError("Argumenty musí být kladná čísla.")
    return n % d


print(je_dělitelné(10, 2))
#print(je_dělitelné(10, 3.5))
#print(je_dělitelné(10, "5"))
0
In [36]:
# takto by mohla vypadat testovací (logovací) funkce, která výjimky odchytne:
def test_and_print(n, d):
    try:
        result = je_dělitelné(n, d)
    except (ValueError, TypeError) as e:
        print(f"{n} {d}: Program nalezl chybu: {e}")
    else:
        print(f"{n} {d}: {result}")

# chybné argumenty:
test_and_print(1, 3.5)
test_and_print('8', 4)
test_and_print(0, 5)
test_and_print(5, 0)

# argumenty jsou v pořádku:
test_and_print(10, 2)
test_and_print(10, 3)
test_and_print(10, 5)
1 3.5: Program nalezl chybu: Argumenty musí být typu int.
8 4: Program nalezl chybu: Argumenty musí být typu int.
0 5: Program nalezl chybu: Argumenty musí být kladná čísla.
5 0: Program nalezl chybu: Argumenty musí být kladná čísla.
10 2: 0
10 3: 1
10 5: 0
In [ ]:
# nápověda k dalšímu příkladu:
In [60]:
chr(98), ord('a')
Out[60]:
('b', 97)
In [61]:
enumerate([1,2,3])
Out[61]:
<enumerate at 0x7c1b6f499df0>
In [63]:
list(enumerate("abc"))
Out[63]:
[(0, 'a'), (1, 'b'), (2, 'c')]
In [64]:
for x, y in enumerate("abc"):
    print(x, y)
0 a
1 b
2 c
In [65]:
[x for x in "abc"]
Out[65]:
['a', 'b', 'c']

3. Indexy¶

Napište funkci find_letter_indexes(string, letter), která vrátí seznam všech indexů, kde se v řetězci string vyskytuje znak char pomocí generátorové notace a funkce enumerate. Rozšiřte řešení této úlohy o kontrolu argumentů funkce: string by měl být neprázdný string a letter by měl být buď string délky jedna nebo se může jednat o číselný kód znaku (typ int). Pokud argumenty nejsou správných typů, funkce by měla vyvolat vhodný typ výjimky (TypeError nebo ValueError) s vhodným textovým popisem chyby. Pokud string znak letter neobsahuje, nechte funkci také vyvolat vhodnou výjimku (ValueError).

In [43]:
def find_letter_indexes(string, letter):
    if not isinstance(string, str):
        raise TypeError("Neplatné argumenty: 'string' musí být typu 'str'.")
    if not ( isinstance(letter, str) or isinstance(letter, int)):
        raise TypeError("Neplatné argumenty: 'letter' musí být typu 'str' nebo 'int'.")
    if isinstance(letter, str) and not len(letter) == 1:
        raise ValueError("String 'letter' musí být string délky jedna nebo celočíselný kód znaku.")
    if isinstance(letter, int):
        letter = chr(letter)
    if  letter not in string:
        raise ValueError(f"Znak {letter} není obsažen ve stringu.")
    return [index for index, char in enumerate(string) if char == letter]


assert find_letter_indexes("hello world", "o") == [4, 7]
assert find_letter_indexes("hello world", 108) == [2, 3, 9]

Vložte volání funkce do pokusného bloku (můžete se inspirovat testovací funkcí u předchozího příkladu) a otestujte funkci na vhodných příkladech:

In [44]:
s = "hello world"
l = "ll"
# result = find_letter_indexes(s, l)

try:
    find_letter_indexes(s, l)
except (ValueError, TypeError) as e:
    print(type(e).__name__, ":", e)

l = 45.0
try:
    find_letter_indexes(s, l)
except (ValueError, TypeError) as e:
    print(type(e).__name__, ":", e)
ValueError : String 'letter' musí být string délky jedna nebo celočíselný kód znaku.
TypeError : Neplatné argumenty: 'letter' musí být typu 'str' nebo 'int'.

4. Známky¶

Seznam studentů je reprezentovaný jako seznam slovníků, kde pro každého studenta je jeho jméno a seznam získaných známek. Příklad správně definovaného seznamu:

In [65]:
grades = [
    {'name': 'Adam', 'grades': [1, 2, 3, 1, 2]},
    {'name': 'Martin', 'grades': [1, 2, 2]},
    {'name': 'Filip', 'grades':  [1, 3]},
    {'name': 'David', 'grades':  [1, 2, 2, 3]},
    {'name': 'Jakub', 'grades':  [1, 2, 2, 1, 1]},
]
  1. Napište funkci check_format(grades), která pomocí assert-ů otestuje, zda proměnná grades má správný formát:

    • grades je typu seznam (list)
    • prvky tohoto seznamu jsou pouze slovníky (dict)
    • každý slovník obsahuje právě klíče name a grades
    • datový typ hodnoty name je str a datový typ grades je list
    • seznam se známkami obsahuje pouze čísla od 1 do 5
    • žádné jméno studenta se v seznamu neopakuje

Doplňte assert-y o vhodné chybové hlášky.

Zkuste pro funkci vymyslet vhodné testovací příklady.

In [ ]:
def check_format(grades):
    ...

check_format(grades)

grades_with_error = (1, 2)

try:
    check_format(grades_with_error)
except AssertionError as e:
    print("Kontrola funguje:")
    print(e)
else:
    assert False, "Kontrola nefunguje."
  1. Napište funkci, která vrátí seznam jmen studentů, kteří nemají dostatek známek (alespoň n). Funkce pomocí funkce check_format() zkontroluje, zda seznam students je správného formátu. Také zkontroluje, zda n je (nezáporné) celé číslo. Jinak vyvolá vhodný typ výjimky.
In [ ]:
def students_with_not_enough_grades(students, n):
    ...

assert students_with_not_enough_grades(grades,4) == ['Martin', 'Filip']
  1. Napište funkci, která vrátí seznam, kde bude pro každé jméno studenta jeho průměr známek. Seznam bude seřazený v pořadí od studenta s nejlepším průměrem známek po studenta s nejhorším průměrem. Pokud seznam students nemá správný formát, funkce nevyvolá výjimku, ale vypíše textový popis chyby a vrátí hodnotu None.
In [68]:
def sort_students_by_average_grade(students):
    ...
   
print(sort_students_by_average_grade(grades))
print(sort_students_by_average_grade(grades_with_error)) # None
None
None

5. Načti a sečti čísla¶

Napište funkci, která postupně načítá čísla ze vstupu, dokud nepřijde něco jiného než číslo. Pak vrátí počet zadaných čísel a jejich součet. Zkuste úlohu řešit s využitím odchytávání výjimky nebo bez něho.

In [69]:
# varianta bez výjimek
def sum_numbers():
    pocet = 0
    soucet = 0
    while True:
        xs = input("zadej cislo")
        if xs.isdigit():
            x = int(xs)
            pocet += 1
            soucet += x
        else:
            break
    return pocet, soucet
        

c, s = sum_numbers()
print(f"Počet čísel je {c} a jejich součet je {s}.")
Počet čísel je 2 a jejich součet je 14.
In [45]:
# varianta s výjimkami
def sum_numbers():
    pocet = 0
    soucet = 0
    while True:
        try:
            x = int(input("zadej cislo"))
            pocet += 1
            soucet += x
        except ValueError:
            break
    return pocet, soucet
        

c, s = sum_numbers()
print(f"Počet čísel je {c} a jejich součet je {s}.")
Počet čísel je 2 a jejich součet je 14.