Ko pride do napake Python sestavi objekt razreda Exception (po slovensko, izjema), oziroma, točneje, enega od razredov, izpeljanih iz Exception. Vsak razred predstavlja svojo vrsto napake. Teh je ogromno, pa še nove si lahko izmišljamo. Ko sestavi ta objekt, ga vrže (throw) oz. sproži (raise). V Pythonu se uporablja slednji izraz, raise, v Cju podobnih jezikih pa prvi, throw. Če vržene izjeme nihče ne ulovi, se ta izpiše, kot smo vajeni. Tule se bomo naučili loviti, potem pa še metati izjeme.

Lovljenje napak

Če predvidevamo, da lahko v določenem delu kode pride do napake (kakor bomo tule po domače govorili izjemam), ga zapremo v blok try-except (in ne try-catch, ko bi se reklo v C-ju in žlahti). Takole je videti.

visina = input("Višina: ")
teza = input("Teža: ")
try:
    visina = float(visina)
    teza = float(teza)
except ValueError:
    print(f"Napačen vnos")
Višina:  velik
Teža:  težak
Napačen vnos

Besedi try sledi dvopičje in, kot smo vajeni, zamaknjen blok, ki seveda lahko vsebuje tudi več kot eno vrstico. Nato pride except, kjer, če smo vljudni, povemo, kakšne vrste napako pričakujemo. Znotraj bloka except storimo kaj pametnega - izpišemo kakšno obvestilo ali kaj podobnega.

Blokov except je lahko tudi več, če pričakujemo več različnih napak.

Če nismo vljudni, izpustimo vrsto napake in pišemo samo except:. Tak except ujame vse napake.

Ko pride do napake, se izvajanje bloka prekine. Python poišče prvi blok except, ki se ujema z vrsto napake in začne izvajati kodo v njem.

Lovi samo predvidene napake

Tule bi se morda zdela boljša ideja:

try:
    visina = input("Višina: ")
    teza = input("Teža: ")
    visina = float(visina)
    teza = float(teza)
    bmi = teza / visina ** 2
    print(f"Indeks telesne teže: {bmi:.2f}")
except ValueError:
    print(f"Napačen vnos")
Višina:  1.85
Teža:  76
Indeks telesne teže: 22.21

Vendar ni, saj bi bila potem dobra ideja tudi to:

try:
    with open("stevilke.txt") as f:
        vrstic = 0
        v = 0
        for vrstica in f:
            v += int(vrstica)
            vrstic += 1
        povp = v / vrstic
        print(f"Povprečje: {povp:.2f}")
except IOError:
    print("Datoteka ne obstaja")
except ValueError:
    print("Napačna vrednost v datoteki")
except ZeroDivisionError:
    print("Datoteka je prazna")
except:
    print("Neznana napaka")
Povprečje: 6.33

Pa ni. To je zelo slaba ideja.

except ne sme loviti preveč na široko. except ValueError smo napisali, da prestrežemo morebitno napako v int(vrstica), torej naj lovi le napako v tej vrstici. Ideja try-except ni loviti napak na počez. Če neke napake nismo predvideli, je boljše, da je ne prestrežemo. Program ne sme teči naprej, kot da je vse v redu, če ni. Še manj sme, recimo, izpisati, da datoteka vsebuje napačno vrednost, če problem morda sploh ni v datoteki, temveč je napaka ValueError priletela od kod drugod.

Loviti z except: pa je sploh skoraj prepovedano. To storite samo, ko kličete neko (tujo) kodo, v kateri se lahko zgodi karkoli.

Pravilneje bi bilo torej tako:

try:
    with open("stevilke.txt") as f:
        vrstic = 0
        v = 0
        for vrstica in f:
            try:
                v += int(vrstica)
            except ValueError:
                print("Napačna vrednost v datoteki")
                continue
            vrstic += 1
        try:
            povp = v / vrstic
        except ZeroDivisionError:
            print("Datoteka je prazna")
            povp = 0  # da se ima v naslednji vrtici kaj izpisati ...
        print(f"Povprečje: {povp:.2f}")
except IOError:
    print("Datoteka ne obstaja")
Povprečje: 6.33

except IOError je neroden, vendar neizogiben, če želimo datoteko odpreti z with. In ja, zoprn je tudi zato, ker lahko (vsaj načelno) pride do IOError tudi ob branju datoteke.

Napakam se raje izogni, kot da jih loviš

Edina napaka, ki jo malo težje predvidimo, je ValueError v int(vrstica). Vse ostale lahko predvidimo in preprečimo.

import os

if not os.path.exists("stevilke.txt"):
    print("Datoteka ne obstaja")
else:
    with open("stevilke.txt") as f:
        vrstic = 0
        v = 0
        for vrstica in f:
            try:
                v += int(vrstica)
            except ValueError:
                print("Napačna vrednost v datoteki")
                continue
            vrstic += 1
        if vrstic == 0:
            print("Datoteka je prazna")
        else:
            povp = v / vrstic
            print(f"Povprečje: {povp:.2f}")
Povprečje: 6.33

Če ni napake ...

Vrnimo se k indeksu telesne teže. Rekli smo, da je tole slaba ideja.

try:
    visina = input("Višina: ")
    teza = input("Teža: ")
    visina = float(visina)
    teza = float(teza)
    bmi = teza / visina ** 2
    print(f"Indeks telesne teže: {bmi:.2f}")
except ValueError:
    print(f"Napačen vnos")
Višina:  1.85
Teža:  76
Indeks telesne teže: 22.21

Kako to popraviti? Imamo namreč problem.

visina = input("Višina: ")
teza = input("Teža: ")
try:
    visina = float(visina)
    teza = float(teza)
except ValueError:
    print(f"Napačen vnos")    
bmi = teza / visina ** 2
print(f"Indeks telesne teže: {bmi:.2f}")
Višina:  1.85
Teža:  76
Indeks telesne teže: 22.21

Problem je v tem: če se zgodi napaka, potem sta visina in teza se vedno niza. Strašno nerodno bi bilo pisati:

visina = input("Višina: ")
teza = input("Teža: ")
try:
    visina = float(visina)
    teza = float(teza)
except ValueError:
    print(f"Napačen vnos") 
    visina = teza = None
    
if visina is not None:
    bmi = teza / visina ** 2
    print(f"Indeks telesne teže: {bmi:.2f}")
Višina:  1.85
Teža:  76
Indeks telesne teže: 22.21

except-u sme slediti else. Kar napišemo znotraj else, se izvede le, če ni prišlo do izjeme.

visina = input("Višina: ")
teza = input("Teža: ")
try:
    visina = float(visina)
    teza = float(teza)
except ValueError:
    print(f"Napačen vnos") 
else:
    bmi = teza / visina ** 2
    print(f"Indeks telesne teže: {bmi:.2f}")

Končno

Končno lahko napišemo tudi finally. Ta lahko sledi exceptu oz. else-u, lahko pa imamo celo samo try in finally.

Kar se nahaja znotraj bloka finally se izvede v vsakem primeru - najsi je prišlo do izjeme ali ne.

try:
    1 / 0
finally:
    print("Zgodilo se je nekaj izjemnega.")
Zgodilo se je nekaj izjemnega.
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
Cell In[25], line 2
      1 try:
----> 2     1 / 0
      3 finally:
      4     print("Zgodilo se je nekaj izjemnega.")

ZeroDivisionError: division by zero

Napaka se izpiše, predtem pa se izvede tudi blok finally.

Več o izjemi

Objekt, ki predstavlja izjemo, lahko vsebuje različne podatke, povezane z njo. Če nas zanimajo, except <vrsta-izjeme> nadaljujemo z as <ime>.

try:
    open("te-datoteke-ni.txt")
except IOError as napaka:
    print(napaka)
    print(napaka.filename)
    print(napaka.strerror)
[Errno 2] No such file or directory: 'te-datoteke-ni.txt'
te-datoteke-ni.txt
No such file or directory

IOError je zelo zgovoren, drugi manj. Vsaj vrsto napake in sporočilo pa lahko vedno izvemo.

Proženje napak

Včasih želimo napako sprožiti sami. To je preprosto. Zapomniti si moramo le, da v Pythonu tega ne storimo s throw, kot v jezikih s Cjevsko sintakso, temveč z raise.

raise ValueError("Čudna vrednost.")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[37], line 1
----> 1 raise ValueError("Čudna vrednost.")

ValueError: Čudna vrednost.