8. Chyby a výjimky

Dosud jsme se o chybových hlášeních pouze zmínili, ale pokud jste si příklady zkoušeli, pravděpodobně jste už některá viděli. Rozlišujeme (přinejmenším) dva druhy chyb: syntaktické chyby a výjimky.

8.1. Syntaktické chyby

Syntaktické chyby, označované také jako chyby při syntaktické analýze, jsou patrně nejčastějším druhem potíží při učení se Pythonu:

>>> while True print('Hello world')
  File "<stdin>", line 1
    while True print('Hello world')
               ^^^^^
SyntaxError: invalid syntax

Syntaktický analyzátor zopakuje chybný řádek a zobrazí malé šipky ukazující na místo, kde byla chyba zjištěna. Nemusí však jít o místo, které je třeba opravit. V příkladu je chyba zjištěna u funkce print(), protože těsně před ní chybí dvojtečka (':').

Vypíše se také název souboru (v našem příkladu <stdin>) a číslo řádku, abyste věděli, kde hledat, pokud vstup pochází ze souboru.

8.2. Výjimky

I syntakticky správný příkaz nebo výraz může při pokusu o provedení způsobit chybu. Chyby zjištěné za běhu se nazývají výjimky a nemusejí být nutně fatální: brzy se naučíte, jak je v programech v Pythonu obsluhovat. Většina výjimek však programy obsluhována není a vede k chybovým hlášením, jako jsou následující:

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    10 * (1/0)
          ~^~
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    4 + spam*3
        ^^^^
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    '2' + 2
    ~~~~^~~
TypeError: can only concatenate str (not "int") to str

Poslední řádek chybového hlášení uvádí, co se stalo. Výjimky mají různé typy a typ je součástí hlášení: v příkladu jde o ZeroDivisionError, NameError a TypeError. Řetězec vypsaný jako typ výjimky je názvem vestavěné výjimky, která nastala. To platí pro všechny vestavěné výjimky, ale nemusí to platit pro uživatelsky definované výjimky (jde však o užitečnou konvenci). Standardní názvy výjimek jsou vestavěné identifikátory, nikoli rezervovaná klíčová slova.

Zbytek řádku uvádí podrobnosti odpovídající typu výjimky a její příčině.

Předchozí část chybového hlášení ukazuje kontext, ve kterém výjimka nastala, formou výpisu zásobníku volání. Obecně obsahuje zdrojové řádky, nezobrazí však řádky načtené ze standardního vstupu.

Část Built-in Exceptions uvádí vestavěné výjimky a jejich význam.

8.3. Obsluha výjimek

Lze psát programy, které obsluhují vybrané výjimky. Následující příklad žádá uživatele o vstup, dokud nezadá platné celé číslo, ale umožňuje mu program přerušit (pomocí Control-C nebo prostředku podporovaného operačním systémem). Přerušení vyvolané uživatelem je signalizováno výjimkou KeyboardInterrupt.

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...         break
...     except ValueError:
...         print("Oops!  That was no valid number.  Try again...")
...

Příkaz try funguje následovně.

  • Nejprve se provede klauzule try (příkaz či příkazy mezi klíčovými slovy try a except).

  • Pokud žádná výjimka nenastane, klauzule except se přeskočí a provádění příkazu try skončí.

  • Pokud během provádění klauzule try nastane výjimka, zbytek klauzule se přeskočí. Odpovídá-li její typ výjimce uvedené za klíčovým slovem except, provede se klauzule except a provádění poté pokračuje za blokem try/except.

  • Pokud nastane výjimka, která neodpovídá výjimce uvedené v klauzuli except, předá se vnějším příkazům try. Nenajde-li se žádná obsluha, jde o neošetřenou výjimku a provádění se zastaví s chybovým hlášením.

Příkaz try může mít více klauzulí except, které určují obsluhu různých výjimek. Provede se nejvýše jedna obsluha. Obsluhy zachycují pouze výjimky, které nastanou v odpovídající klauzuli try, nikoli v jiných obsluhách téhož příkazu try. Klauzule except může uvádět více výjimek, například:

... except RuntimeError, TypeError, NameError:
...     pass

Třída v klauzuli except odpovídá výjimkám, které jsou instancemi této třídy nebo některé z jejích odvozených tříd (nikoli však naopak — klauzule except uvádějící odvozenou třídu neodpovídá instancím jejích základních tříd). Následující kód například vypíše B, C, D v tomto pořadí:

class B(Exception):
    pass

class C(B):
    pass

class D(C):
    pass

for cls in [B, C, D]:
    try:
        raise cls()
    except D:
        print("D")
    except C:
        print("C")
    except B:
        print("B")

Kdyby bylo pořadí klauzulí except obrácené (s except B jako první), kód by vypsal B, B, B — spustí se první odpovídající klauzule except.

Výjimka může mít přidružené hodnoty, označované také jako její argumenty. Přítomnost a typy argumentů závisejí na typu výjimky.

Klauzule except může za názvem výjimky uvést proměnnou. Ta se naváže na instanci výjimky, která obvykle obsahuje atribut args uchovávající argumenty. Vestavěné typy výjimek pro usnadnění definují __str__(), která vypíše všechny argumenty bez přímého přístupu k .args.

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))    # the exception type
...     print(inst.args)     # arguments stored in .args
...     print(inst)          # __str__ allows args to be printed directly,
...                          # but may be overridden in exception subclasses
...     x, y = inst.args     # unpack args
...     print('x =', x)
...     print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

Výstup metody __str__() výjimky se vypíše jako poslední část („podrobnosti“) hlášení o neošetřené výjimce.

BaseException je společnou základní třídou všech výjimek. Jedna z jejích podtříd, Exception, je základní třídou všech nefatálních výjimek. Výjimky, které nejsou podtřídami Exception, se obvykle neobsluhují, protože signalizují, že by měl program skončit. Patří mezi ně SystemExit, kterou vyvolává sys.exit(), a KeyboardInterrupt, která se vyvolá, když chce uživatel program přerušit.

Exception lze použít jako zástupný typ, který zachytí (téměř) vše. Je však vhodné co nejpřesněji určit typy výjimek, které chceme obsloužit, a umožnit neočekávaným výjimkám, aby se šířily dál.

Nejběžnějším vzorem pro obsluhu Exception je výjimku vypsat nebo zaznamenat do protokolu a poté ji znovu vyvolat (takže ji může obsloužit také volající):

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("OS error:", err)
except ValueError:
    print("Could not convert data to an integer.")
except Exception as err:
    print(f"Unexpected {err=}, {type(err)=}")
    raise

Příkaz tryexcept má nepovinnou klauzuli else, která musí následovat za všemi klauzulemi except. Je užitečná pro kód, který se má provést, pokud klauzule try nevyvolá výjimku. Například:

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except OSError:
        print('cannot open', arg)
    else:
        print(arg, 'has', len(f.readlines()), 'lines')
        f.close()

Použití klauzule else je lepší než přidání dalšího kódu do klauzule try, protože zabraňuje neúmyslnému zachycení výjimky, kterou nevyvolal kód chráněný příkazem tryexcept.

Obsluhy výjimek nezachycují pouze výjimky, které nastanou přímo v klauzuli try, ale také výjimky uvnitř funkcí volaných z této klauzule, a to i nepřímo. Například:

>>> def this_fails():
...     x = 1/0
...
>>> try:
...     this_fails()
... except ZeroDivisionError as err:
...     print('Handling run-time error:', err)
...
Handling run-time error: division by zero

8.4. Vyvolávání výjimek

Příkaz raise umožňuje programátorovi vynutit vyvolání určené výjimky. Například:

>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    raise NameError('HiThere')
NameError: HiThere

Jediný argument příkazu raise určuje výjimku, která se má vyvolat. Musí jít buď o instanci výjimky, nebo o třídu výjimky (třídu odvozenou od BaseException, například Exception nebo některou z jejích podtříd). Je-li předána třída výjimky, implicitně se vytvoří její instance voláním konstruktoru bez argumentů:

raise ValueError  # shorthand for 'raise ValueError()'

Potřebujete-li zjistit, zda byla výjimka vyvolána, ale nechcete ji obsluhovat, umožňuje jednodušší tvar příkazu raise výjimku znovu vyvolat:

>>> try:
...     raise NameError('HiThere')
... except NameError:
...     print('An exception flew by!')
...     raise
...
An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    raise NameError('HiThere')
NameError: HiThere

8.5. Řetězení výjimek

Pokud uvnitř části except nastane neošetřená výjimka, připojí se k ní právě obsluhovaná výjimka a obě budou zahrnuty v chybovém hlášení:

>>> try:
...     open("database.sqlite")
... except OSError:
...     raise RuntimeError("unable to handle error")
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    open("database.sqlite")
    ~~~~^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
    raise RuntimeError("unable to handle error")
RuntimeError: unable to handle error

K vyjádření, že je jedna výjimka přímým důsledkem jiné, umožňuje příkaz raise použít nepovinnou klauzuli from:

# exc must be exception instance or None.
raise RuntimeError from exc

To může být užitečné při převádění výjimek. Například:

>>> def func():
...     raise ConnectionError
...
>>> try:
...     func()
... except ConnectionError as exc:
...     raise RuntimeError('Failed to open database') from exc
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    func()
    ~~~~^^
  File "<stdin>", line 2, in func
ConnectionError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
    raise RuntimeError('Failed to open database') from exc
RuntimeError: Failed to open database

Automatické řetězení výjimek lze také vypnout pomocí idiomu from None:

>>> try:
...     open('database.sqlite')
... except OSError:
...     raise RuntimeError from None
...
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
    raise RuntimeError from None
RuntimeError

Další informace o mechanismu řetězení naleznete v části Built-in Exceptions.

8.6. Uživatelsky definované výjimky

Programy mohou pojmenovat vlastní výjimky vytvořením nové třídy výjimky (další informace o třídách v Pythonu viz Třídy). Výjimky by měly být zpravidla přímo či nepřímo odvozeny od třídy Exception.

Třídy výjimek mohou dělat cokoli, co jiné třídy, obvykle však zůstávají jednoduché a často nabízejí pouze několik atributů, pomocí nichž mohou obsluhy výjimek získat informace o chybě.

Názvy většiny výjimek končí slovem „Error“, podobně jako názvy standardních výjimek.

Mnoho standardních modulů definuje vlastní výjimky pro oznamování chyb, které mohou nastat v jimi definovaných funkcích.

8.7. Definování úklidových akcí

Příkaz try má další nepovinnou klauzuli určenou k definování úklidových akcí, které se musí provést za všech okolností. Například:

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print('Goodbye, world!')
...
Goodbye, world!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    raise KeyboardInterrupt
KeyboardInterrupt

Je-li přítomna klauzule finally, provede se jako poslední úloha před dokončením příkazu try. Klauzule finally se provede bez ohledu na to, zda příkaz try vyvolá výjimku. Následující body popisují složitější případy, kdy výjimka nastane:

  • Nastane-li výjimka během provádění klauzule try, může ji obsloužit klauzule except. Pokud ji žádná klauzule except neobslouží, po provedení klauzule finally se výjimka znovu vyvolá.

  • Výjimka může nastat během provádění klauzule except nebo else. I v tomto případě se po provedení klauzule finally znovu vyvolá.

  • Pokud klauzule finally provede příkaz break, continue nebo return, výjimky se znovu nevyvolají. To může být matoucí, a proto se takový postup nedoporučuje. Od verze 3.14 na něj kompilátor upozorňuje pomocí SyntaxWarning (viz PEP 765).

  • Pokud příkaz try dospěje k příkazu break, continue nebo return, klauzule finally se provede těsně před provedením daného příkazu break, continue nebo return.

  • Pokud klauzule finally obsahuje příkaz return, vrácená hodnota bude pocházet z příkazu return v klauzuli finally, nikoli z příkazu return v klauzuli try. To může být matoucí, a proto se takový postup nedoporučuje. Od verze 3.14 na něj kompilátor upozorňuje pomocí SyntaxWarning (viz PEP 765).

Například:

>>> def bool_return():
...     try:
...         return True
...     finally:
...         return False
...
>>> bool_return()
False

Složitější příklad:

>>> def divide(x, y):
...     try:
...         result = x / y
...     except ZeroDivisionError:
...         print("division by zero!")
...     else:
...         print("result is", result)
...     finally:
...         print("executing finally clause")
...
>>> divide(2, 1)
result is 2.0
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    divide("2", "1")
    ~~~~~~^^^^^^^^^^
  File "<stdin>", line 3, in divide
    result = x / y
             ~~^~~
TypeError: unsupported operand type(s) for /: 'str' and 'str'

Jak vidíte, klauzule finally se provede za všech okolností. Výjimku TypeError vyvolanou dělením dvou řetězců klauzule except neobslouží, a proto se po provedení klauzule finally znovu vyvolá.

V reálných aplikacích je klauzule finally užitečná k uvolňování externích prostředků (například souborů nebo síťových spojení) bez ohledu na to, zda bylo jejich použití úspěšné.

8.8. Předdefinované úklidové akce

Některé objekty definují standardní úklidové akce, které se mají provést, když objekt již není zapotřebí, bez ohledu na to, zda operace používající objekt uspěla, či selhala. Následující příklad se pokusí otevřít soubor a vypsat jeho obsah na obrazovku.

for line in open("myfile.txt"):
    print(line, end="")

Problém tohoto kódu spočívá v tom, že po dokončení dané části kódu ponechá soubor po neurčenou dobu otevřený. U jednoduchých skriptů to nevadí, ale ve větších aplikacích to může být problém. Příkaz with umožňuje používat objekty, jako jsou soubory, způsobem, který zajišťuje jejich vždy včasný a správný úklid.

with open("myfile.txt") as f:
    for line in f:
        print(line, end="")

Po provedení příkazu je soubor f vždy zavřen, i když při zpracování řádků nastal problém. Objekty, které podobně jako soubory poskytují předdefinované úklidové akce, na tuto skutečnost upozorňují ve své dokumentaci.

8.9. Vyvolávání a obsluha více nesouvisejících výjimek

Existují situace, kdy je nutné oznámit několik výjimek, které nastaly. Často se to stává v systémech pro souběžné zpracování, kde může paralelně selhat několik úloh, existují však i jiné případy, kdy je vhodné pokračovat v provádění a shromáždit více chyb namísto vyvolání první výjimky.

Vestavěná výjimka ExceptionGroup obaluje seznam instancí výjimek, aby je bylo možné vyvolat společně. Sama je výjimkou, takže ji lze zachytit stejně jako jakoukoli jinou výjimku.

>>> def f():
...     excs = [OSError('error 1'), SystemError('error 2')]
...     raise ExceptionGroup('there were problems', excs)
...
>>> f()
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |     f()
  |     ~^^
  |   File "<stdin>", line 3, in f
  |     raise ExceptionGroup('there were problems', excs)
  | ExceptionGroup: there were problems (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | OSError: error 1
    +---------------- 2 ----------------
    | SystemError: error 2
    +------------------------------------
>>> try:
...     f()
... except Exception as e:
...     print(f'caught {type(e)}: {e}')
...
caught <class 'ExceptionGroup'>: there were problems (2 sub-exceptions)
>>>

Použitím except* namísto except můžeme selektivně obsloužit pouze výjimky ve skupině, které odpovídají určitému typu. V následujícím příkladu s vnořenou skupinou výjimek každá klauzule except* vybere ze skupiny výjimky určitého typu, zatímco všechny ostatní nechá šířit do dalších klauzulí a nakonec znovu vyvolat.

>>> def f():
...     raise ExceptionGroup(
...         "group1",
...         [
...             OSError(1),
...             SystemError(2),
...             ExceptionGroup(
...                 "group2",
...                 [
...                     OSError(3),
...                     RecursionError(4)
...                 ]
...             )
...         ]
...     )
...
>>> try:
...     f()
... except* OSError as e:
...     print("There were OSErrors")
... except* SystemError as e:
...     print("There were SystemErrors")
...
There were OSErrors
There were SystemErrors
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 2, in <module>
  |     f()
  |     ~^^
  |   File "<stdin>", line 2, in f
  |     raise ExceptionGroup(
  |     ...<12 lines>...
  |     )
  | ExceptionGroup: group1 (1 sub-exception)
  +-+---------------- 1 ----------------
    | ExceptionGroup: group2 (1 sub-exception)
    +-+---------------- 1 ----------------
      | RecursionError: 4
      +------------------------------------
>>>

Výjimky vnořené do skupiny výjimek musí být instance, nikoli typy. V praxi totiž zpravidla půjde o výjimky, které již program vyvolal a zachytil podle následujícího vzoru:

>>> excs = []
... for test in tests:
...     try:
...         test.run()
...     except Exception as e:
...         excs.append(e)
...
>>> if excs:
...    raise ExceptionGroup("Test Failures", excs)
...

8.10. Obohacování výjimek poznámkami

Když je vytvořena výjimka určená k vyvolání, obvykle se inicializuje informacemi popisujícími vzniklou chybu. V některých případech je užitečné přidat informace až po zachycení výjimky. K tomu mají výjimky metodu add_note(note), která přijme řetězec a přidá jej do seznamu poznámek výjimky. Standardní zobrazení výpisu zásobníku zahrnuje za výjimkou všechny poznámky v pořadí, v jakém byly přidány.

>>> try:
...     raise TypeError('bad type')
... except Exception as e:
...     e.add_note('Add some information')
...     e.add_note('Add some more information')
...     raise
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    raise TypeError('bad type')
TypeError: bad type
Add some information
Add some more information
>>>

Při shromažďování výjimek do skupiny můžeme například chtít přidat kontextové informace k jednotlivým chybám. V následujícím příkladu má každá výjimka ve skupině poznámku uvádějící, kdy daná chyba nastala.

>>> def f():
...     raise OSError('operation failed')
...
>>> excs = []
>>> for i in range(3):
...     try:
...         f()
...     except Exception as e:
...         e.add_note(f'Happened in Iteration {i+1}')
...         excs.append(e)
...
>>> raise ExceptionGroup('We have some problems', excs)
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |     raise ExceptionGroup('We have some problems', excs)
  | ExceptionGroup: We have some problems (3 sub-exceptions)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 1
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 2
    +---------------- 3 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 3
    +------------------------------------
>>>