7. seminář - Ošetřování chyb při běhu programu

TEACHINGZPP2

Pokud při běhu programu nastane chyba (například při dělení nulou), je vykonávání programu okamžitě zastaveno a následně dojde k vyvolání tzv. výjimky. Tu si lze pro jednoduchost představit jako informační zprávu o vzniklé chybě. Pokud tato zpráva není programem zachycena (zpracována) program skončí chybou. Například.


    def division(number, divisor):
        return number / divisor

    print(division(42, 0)) # způsobí chybu: ZeroDivisionError: division by zero

Příkazy try a except

Příkazy try a except jsou základní příkazy pro ošetření výjimek. Obecný zápis vypadá následovně.


    try:
        commands_1
    except:
        commands_2

Příkaz try vymezuje blok kódu příkazy_1, běžně označovaný jako try-blok, při jehož vykonávání jsou zachytávány výjimky. Příkaz except přidružený k try-bloku specifikuje blok kódu, označíme jej except-blok, který je vykonán v případě, že v bloku commands_1 došlo k vyvolání výjimky. Pomocí try a except můžeme funkci division() upravit následovně.


    def division(number, divisor):
        try:
            return number / divisor
        except:
            return None
            
    division(42, 0) # vrátí None

Upřesnění typu výjimky

K try-bloku přidružený příkaz except ošetřuje (stejně) všechny výjimky. V některých případech ale chceme, aby byly různé výjimky ošetřeny různým způsobem. Bezprostředně za příkaz except je možné uvést jméno výjimky, čímž určujeme, že příslušný blok kódu ošetřuje výjimku daného jména (typu). K jednomu try-bloku může být přidruženo více příkazů except. Rozšířený zápis vypadá následovně.


    try: 
        commands
    except name_1:
        commands_1
    except name_2:
        commands_2
    except:
        commands_3

V případě, že dojde v try-bloku příkazy k vyvolání výjimky, jsou postupně procházeny přidružené příkazy except. Pokud název vyvolané výjimky odpovídá name_1, je vykonán blok commands_1. V opačném případě se analogicky pokračuje s dalším příkazem except. Dodejme, že ve výše uvedeném je poslední příkaz except nepovinný. Názvy nejběžnějších výjimek v jazyce Python jsou shrnuty níže. Následující kód ukazuje ošetření několika různých výjimek při provádění funkce division().

  • AssertionError - podmínka v příkazu aserce (vysvětlíme později) není splněna
  • IndexError - přístup na neexistující index v kolekci
  • NameError - přístup k nedefinované proměnné
  • TypeError - operace s nekompatibilními datovými typy, například: "4"+ 2
  • ValueError - operace s kompatibilními datovými typy ale s chybnou hodnotou, například: int("dva")
  • OverflowError - výsledek operace je příliš velký a nelze jej reprezentovat v paměti
  • ZeroDivisionError - druhý operand operace dělení nebo modulo je roven nule
  • RuntimeError - blíže nespecifikovaná chyba
  • Exception - obecná výjimka (zahrnuje všechny výjimky)

    def division(number, divisor):
        try:
            return number / divisor
        except ZeroDivisionError:
            print("nelze dělit nulou")
        except TypeError:
            print("nekompatibilní datové typy")
        except:
            print("nespecifikovaná výjimka:")
    
    division(42, 0) # vypíše: nelze dělit nulou
    division(42, "dva") # vypíše: nekompatibilní datové typy
    division(10**1000,2) # vypíše: nespecifikovaná výjimka

Ke každé vyjímce je přidružen i krátký komentář o chybě která nastala. Pokud chceme s tímto komentářem dále pracovat, tak si vyjímku musíme uložit do proměnné a to následovně:


    except exception_name as variable:
        commands

    def division(number, divisor):
        try:
            return number / divisor
        except ZeroDivisionError:
            print("nelze dělit nulou")
        except TypeError:
            print("nekompatibilní datové typy")
        except Exception as e:
            print("nespecifikovaná výjimka:", e):
    
    division(10**1000,2) # vypíše: nespecifikovaná výjimka: integer division result too large for a float

Manuální vyvolávání výjimky

Doposud jsme uvažovali pouze výjimky, které vznikly při chybě. Výjimky je možné manuálně vyvolat pomocí příkazu raise, jehož použití je ukázáno níže.


    raise exception_name
    
    # Muzeme vyjimku vyvolat i s komentarem
    raise exception_name("komentář")
    
    # Příklad
    
    def division(number, divisor):
        try:
            if divisor == 0:
                raise ZeroDivisionError("operand b ve funkci deleni() je roven 0")
            return number / divisor
        except ZeroDivisionError as e:
            print(e)
    
    division(42, 0) # vypíše: operand b ve funkci division() je roven 0

Vnořené try-bloky

Try-blok může obsahovat další (vnořené) try-bloky. Pokud není daná výjimka ošetřena v try-bloku je předána do nadřazeného try-bloku. Pokud žádný takový není, program končí neošetřenou chybou. Předávání výjimek mezi vnořenými try-bloky se označuje jako propagace výjimek.


    def division(number, divisor):
        try:
            return number / divisor
        except RuntimeError: # tato výjimka při dělení nenastane
            print("RuntimeError")
        
    try:
        division(42, 0)
    except Exception as e:
        print("chyba:", e) # vypíše: chyba: division by zero
        
    
    # Vyvolani vyjimky misto vypisu chyby
    def division(number, divisor):
        try:
            return number / divisor
        except:
            raise RuntimeError("chyba ve funkci division()")
    
    # dojde k vypsání: chyba ve funkci deleni()
    try:
        division(42, 0)
    except Exception as e:
        print(e)

Příkaz finally

Jazyk Python, stejně jako většina jiných programovacích jazyků, které podporují výjimky, disponuje příkazem finally, jenž je stejně jako příkazy exception přidružen k nějakému try-bloku. Příkaz finally určuje blok kódu, označíme jej finally-blok, který je vykonán vždy a to bez ohledu na to, zda byl try-blok opuštěn vyvolanou výjimkou či nikoliv. Příkaz finally se zapisuje za poslední příkaz except.


    try:
        commands_1
    except:
        commands_2
    finally:
        commands_3
        
    # Finally muzeme pouzit i bez except
    
    try:
        commands_1
    finally:
        commands_2
    

Kód ve finally-bloku se běžně používá pro „úklid“ po kódu v try-bloku k němuž je příkaz finally přidružen.

Příkaz assert

V programování se podmínky, o kterých předpokládáme, že jsou za všech okolností pravdivé, označují jako aserce. Aserce se používají pro kontrolu, zda jsou vždy splněny dané předpoklady. Pro zápis asercí se v jazyce Python používá příkaz assert.


    # Pokud je vyraz pravdivy program pokracuje dal, jinak vyvola vyjimku AssertationError s komentarem
    assert command, "komentář"
    
    x = 46
    assert x == 42, "x != 42" # způsobí chybu: AssertionError: x != 42

Výjimky nejsou jen chyby

Mnoho programátorů nesprávně spojuje výjimky pouze s chybami. Výjimka je zpráva informující o tom, že nastala nějaká situace. Vyjimky tedy nemusí být nutně vyvolány pouze při chybě. Následující příklad ukazuje, jak je možné využít výjimku pro okamžité opuštění několika vnořených cyklů.


    l = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
    find = 2
    found = False
    
    try:
        for item_in_l in l:
            for item in item_in_l:
                if item == find:
                    found = True
                    raise Exception("položka nalezena")
    except:
        print(nalezeno) # vypíše True
        
    
    # Výše uvedené by mělo být použito pouze v opodstatněných případech.

Úkoly

Dřívější úkoly doplňte o výjimky.