Nauč se Python > Kurzy > Digitální akademie Czechitas - přípravka Pythonu > Moduly a výjimky > Výjimky

Výjimky

Pojďme si prohloubit znalosti o chybách, neboli odborně o výjimkách (angl. exceptions).

Vezmi následující funkci:

def nacti_cislo():
    odpoved = input('Zadej číslo: ')
    return int(odpoved)

Když uživatel nezadá číslice, ale třeba text cokolada, nastane výjimka jménem ValueError (chyba hodnoty) a Python vypíše odpovídající chybovou hlášku.

Traceback (most recent call last):
  File "ukazka.py", line 3, in nacti_cislo
    cislo = int(odpoved)
ValueError: invalid literal for int() with base 10: 'cokolada'

Program volá funkci int() pro něco, co nedává smysl jako číslo. Co s tím má chudák funkce int dělat? Není žádná rozumná hodnota, kterou by mohla vrátit. Převádění tohoto textu na celé číslo nedává smysl.

Až funkce nacti_cislo nejlíp „ví“, co se má stát, když uživatel nezadá číslice. Stačí se uživatele zeptat znovu! Kdybys měla funkci, která zjistí jestli jsou v řetězci jen číslice, mohlo by to fungovat nějak takhle:

def nacti_cislo():
    while True:
        odpoved = input('Zadej číslo: ')
        if obsahuje_jen_cislice(odpoved):
            return int(odpoved)  # máme výsledek, funkce končí
        else:
            print('To nebylo číslo!')
            # ... a zeptáme se znovu -- cyklus `while` pokračuje

Kde ale vzít funkci obsahuje_jen_cislice? Nemá smysl ji psát znovu – funkce int sama nejlíp pozná, co se dá převést na číslo a co ne. A dokonce nám to dá vědět – chybou, kterou můžeš zachytit.

Ono „obsahuje_jen_cislice“ v Pythonu existuje. Dokonce několikrát. Místo řešení problému to ale spíš ilustruje, v čem problém spočívá:

  • Řetězcová metoda isnumeric vrací True pokud řetězec obsahuje číslice: '123'.isnumeric() je pravda; 'abc'.isnumeric() nepravda. Problém je, že funkci int potřebuje jeden konkrétní druh číslic: pro řetězce jako '½' nebo '௩三๓໓' (trojka v tamilském, japonském, thajském nebo laoském písmu) platí isnumeric, ale int si na nich vyláme zuby stejně jako na 'abc'.
  • Řetězcová metoda isdecimal vrací True pokud řetězec obsahuje arabské číslice 0-9. To už je lepší, ale stejně to úplně nesedí: int si poradí s mezerou na začátku, např. s ' 3', ale funkce isdecimal takový řetězec odmítne.

Chceš-li zjistit jestli funkce int umí daný řetězec převést na číslo, nejlepší je použít přímo funkci int.

Ošetření chyby

Pro zachycení chyby má Python příkaz try/except.

def nacti_cislo():
    while True:
        odpoved = input('Zadej číslo: ')
        try:
            return int(odpoved)
        except ValueError:
            print('To nebylo číslo!')

Jak to funguje? Příkazy v bloku uvozeném příkazem try se normálně provádějí, ale když nastane uvedená výjimka, Python přeskočí zbytek bloku try a provede všechno v bloku except. Pokud výjimka nenastala, přeskočí se celý blok except.

Druhy chyb

A co je to ValueError? To je typ chyby. Podobných typů je spousta. Všechny jsou popsané v dokumentaci; pro nás jsou (nebo budou) důležité tyto:

BaseException
 ├── SystemExit                     vyvolána funkcí exit()
 ├── KeyboardInterrupt              vyvolána po stisknutí Ctrl+C
 ╰── Exception
      ├── ArithmeticError
      │    ╰── ZeroDivisionError    dělení nulou
      ├── AssertionError            nepovedený příkaz `assert`
      ├── AttributeError            neexistující atribut/metoda, např. 'abc'.len
      ├── ImportError               nepovedený import
      ├── LookupError
      │    ╰── IndexError           neexistující index, např. 'abc'[999]
      ├── NameError                 použití neexistujícího jména proměnné
      │    ╰── UnboundLocalError    použití proměnné, která ještě nebyla nastavená
      ├── SyntaxError               špatná syntaxe, program je nečitelný/nepoužitelný
      │    ╰── IndentationError     špatné odsazení
      │         ╰── TabError        kombinování mezer a tabulátorů v odsazení
      ├── TypeError                 špatný typ, např. len(9)
      ╰── ValueError                špatná hodnota, např. int('xyz')

Tohle si není potřeba pamatovat – druh chyby, kterou je potřeba zachytit, vždy najdeš v příslušné chybové hlášce.

Když odchytáváš obecnou výjimku, chytnou se i všechny podřízené typy výjimek – například except ArithmeticError: zachytí i ZeroDivisionError. A except Exception: zachytí všechny výjimky, které běžně chceš zachytit.

Nechytej je všechny!

Většinu chyb není potřeba ošetřovat.

Nastane-li nečekaná situace, je téměř vždy mnohem lepší program ukončit, než se snažit pokračovat dál a počítat se špatnými hodnotami. Navíc chybový výstup, který Python standardně připraví, může hodně ulehčit hledání chyby.

Zachytávej tedy jenom ty chyby, které očekáváš – víš přesně, která chyba může nastat a proč; máš možnost správně zareagovat.

V našem příkladu to platí pro ValueError z funkce int: víš že uživatel nemusí vždy zadat číslo ve správném formátu a víš že správná reakce na tuhle situaci je problém vysvětlit a zeptat se znovu.

Co ale dělat, kdyš uživatel chce ukončit program a zmáčkne Ctrl+C? Nebo když se mu porouchá klávesnice a selže funkce input? Nejlepší reakce na takovou nečekanou situaci ukončit program a informovat uživatele (nebo lépe, programátora), že (a kde) je něco špatně. Neboli vypsat chybovou hlášku. A to se stane normálně, bez try.

Další přílohy k try

Pro úplnost: kromě except existují dva jiné bloky, které můžeš „přilepit“ k try, a to else a finally. První se provede, když v try bloku žádná chyba nenastane; druhý se provede vždy – ať už chyba nastala nebo ne.

Můžeš taky použít více bloků except. Provede se vždy maximálně jeden: ten první, který danou chybu umí ošetřit.

try:
    neco_udelej()
except ValueError:
    print('Tohle se provede, pokud nastane ValueError')
except NameError:
    print('Tohle se provede, pokud nastane NameError')
except Exception:
    print('Tohle se provede, pokud nastane jiná chyba')
    # (kromě SystemExit a KeyboardInterrupt, ty chytat nechceme)
except TypeError:
    print('Tohle se neprovede nikdy')
    # ("except Exception" výše ošetřuje i TypeError; sem se Python nedostane)
else:
    print('Tohle se provede, pokud chyba nenastane')
finally:
    print('Tohle se provede vždycky; i pokud v `try` bloku byl např. `return`')

Vyvolání chyby

Občas se stane, že výjimku budeš potřebovat vyvolat sama.

Často se to stává když píšeš nějakou obecnou funkci. Třeba funkci na výpočet obsahu čtverce. Co se stane, když někdo zavolá obsah_ctverce(-5)?

  • Zadal-li ono -5 uživatel, je potřeba mu vynadat a zeptat se znovu.
  • Naměřil-li -5 nějaký robotický aparát, je potřeba ho líp zkalibrovat.
  • Vyšel-li čtverec se stranou -5 v nějakém výpočtu, je nejspíš potřeba opravit chybu v tom výpočtu.

Samotná funkce obsah_ctverce ale „neví“, proč ji někdo volá. Jejím úkolem je jen něco spočítat. Měla by být použitelná ve všech případech výše – a v mnoha dalších.

Když někdo zavolá obsah_ctverce(-5), neexistuje správný výsledek, který by funkce mohla vrátit. Místo vrácení výsledku musí tato funkce signalizovat chybu. S tou se pak může program, který obsah_ctverce(-5) zavolal, vypořádat – vynadat uživateli, zkalibrovat měřák, nebo, pokud na chybu není připravený, sám skončit s chybou (a upozornit tak programátora, že je něco špatně).

Jak na to prakticky? Chybu můžeš vyvolat pomocí příkazu raise. Za příkaz dáš druh výjimky a pak do závorek nějaký popis toho, co je špatně.

def obsah_ctverce(strana):
    if strana > 0:
        return strana ** 2
    else:
        raise ValueError(f'Strana musí být kladná, číslo {strana} kladné není!')

Podobně jako return, i příkaz raise ukončí funkci. A nejen tu – pokud na tuhle konkrétní chybu není program předem připravený, ukončí se celý program.

Ze začátku není u raise příliš důležité dumat nad tím, který typ výjimky je ten správný. Klidně „střílej od boku“. ValueError bývá často správná volba.


Toto je stránka lekce z kurzu, který probíhá nebo proběhl naživo s instruktorem. Přejít na stejnou lekci v kurzu pro samouky.