Letos smo se naučili že veliko o tem, kako v resnici deluje Python - kam daje spremenljivke, kaj so funkcije, kako pravzaprav deluje zanka for ... Vsaka stvar deluje na nek način, o vsaki stvari se lahko vprašamo "kako je pa to pravzaprav narejeno". Danes bomo rekli nekaj o tem, kaj se zgodi, ko se zgodi napaka.

Ko pride do napake Python sestavi objekt razreda Exception (po slovensko, izjema), ki predstavlja to napako. Pravzaprav ne točno Exception, temveč objekt, ki pripada enemu iz izpeljanih razredov - vsak razred predstavlja drug tip napake.Teh je ogromno. Eden, na primer, je ZeroDivisionError: tega sestavi, kadar poskušamo deliti z 0.

Ko sestavi ta objekt, ga vrže (throw) oz. sproži (raise). (V Pythonu se uporablja slednji izraz, v Cju podobnih jezikih pa prvi.) Python nato prepusti ta objekt prvemu, ki lahko obravnava to napako - prvemu, ki je računal na to, da lahko pride do te napake, in v zvezi z njo kaj ukrene. Če ni nikogar, ki bi znal kaj ukreniti, pa dobimo sporočilo o napaki, kakršnih smo vajeni.

Kako pa ta, ki bi lahko kaj ukrenil, pove, da je zmožen kaj ukreniti?

Lovljenje izjem

Napišimo program, ki vpraša uporabnika po dolžini stranice kvadrata in izpiše njegovo ploščino.

s = input("Vnesi dolžino stranice: ")
a = float(s)
print("Ploščina kvadrata s stranico {} je {}.".format(a, a ** 2))

Tole smo počeli nekako od mladih programerskih nog. Če uporabnik namesto številke vtipka svojo najljubšo barvo, se bo program sesul in to smo vzeli v zakup. Je mar naša odgovornost, da pazimo na to, kaj tipkajo trapasti uporabniki? Ehm: ponavadi je. Tudi Word se ne sesuje, če za velikost razmika med vrsticami ne vtipkate spodobne številke, temveč nas opozori na napako.

Takole.

try:
    s = input("Vnesi dolžino stranice: ")
    a = float(s)
except:
    print("Bi bili tako prijazni in, prosim, vnesli številko?"
print("Ploščina kvadrata s stranico {} je {}.".format(a, a ** 2))

Če se med try in except zgodi napaka, se bo izvedlo to, kar smo napisali znotraj excepta.

Če torej uporabnik vpiše kaj, kar ni število, recimo modra, se bo ob klicu float(s) zgodila napaka, konkretno ValueError: could not convert string to float: 'modra'. To pomeni, da Python sestavi objekt razreda ValueError, ki vsebuje sporočilo "could not convert string to float: 'modra'" in ga "vrže" ... do našega excepta. (V nekaterih jezikih se v ta namen dejansko ne uporablja beseda except temveč catch.)

Če poskušamo pognati tale program, ne deluje čisto prav: če uporabnik vnese nekaj, kar ni številka, sicer ne dobimo takšne napake, kot smo jo prej, pač pa dobimo drugačno - a ni definiran.

Izvajanje programa znotraj bloka try se ob napaki prekine. O tem se bomo prepričali takole:

try:
    s = input("Vnesi dolžino stranice: ")
    a = float(s)
    print("Tole je uspelo!")
except:
    print("Bi bili tako prijazni in, prosim, vnesli številko?")

Če vtipkamo številko, program izpiše sporočilo o uspehu. Če ne, izpiše, naj bomo prijazni - o uspehu pa ničesar. Ker v vrstici s float(s) pride do napake se vse preostale vrstice (konkretno, ena preostala vrstica) preskočijo. Še več, preskoči se že prirejanje aju, kar je edino logično, saj niti ni jasno, kaj bi mu priredili, ko pa je iz funkcije float priplavala izjema namesto številke.

Ko smo torej v prejšnji različici programa po uspešno obravnavani napaki hoteli izpisati ploščino, to ni šlo, ker se je napaka zgodila še pred prirejanjem in spremenljivka a ni obstajala.

To se da seveda urediti z

try:
    s = input("Vnesi dolžino stranice: ")
    a = float(s)
except:
    print("Bi bili tako prijazni in, prosim, vnesli številko?")
    a = 42
print("Ploščina kvadrata s stranico {} je {}.".format(a, a ** 2))

vendar bi bilo to z vidika uporabnika malo nepričakovano: če vnese neumnost, mu program pove, kakšna je ploščina trikotnika s stranico 42. Boljše bi bilo, če bi uporabnika spraševali, dokler ne vnese številke. To se lahko naredi, recimo, tako

while True:
    try:
        s = input("Vnesi dolžino stranice: ")
        a = float(s)
        break
    except:
        print("Bi bili tako prijazni in, prosim, vnesli številko?")
print("Ploščina kvadrata s stranico {} je {}.".format(a, a ** 2))

Ukaz break se bo izvedel, če (oziroma ko) bo program uspel pretvoriti uporabnikov vpis v številko. Dotlej pa se bo vrtel v neskončni zanki.

Še lepše bi bilo to dati v funkcijo.

def vnos(sporocilo):
    while True:
        try:
            return float(input(sporocilo))
        except:
            print("Bi bili tako prijazni in, prosim, vnesli številko?")

Takšnole funkcijo lahko potem uporabljamo za vsa vnašanja števil.

Različne vrste izjem

Napišimo funkcijo, ki kot argument prejme datoteko, ki vsebuje števila, v vsaki vrstici po eno. Funkcija naj vrne njihovo poprečje.

def poprecje(ime):
    s = 0
    c = 0
    for v in open(ime):
        s += float(v)
        c += 1
    return s / c

Kaj vse lahko gre narobe tu? Lahko se zgodi, da datoteka ne obstaja. V tem primeru se bo sprožila izjema pri open(ime). Lahko se zgodi, da kake vrstice ni mogoče pretvoriti v število in napaka se bo sprožila ob float(v). Lahko se zgodi, da je datoteka prazna; c bo enak 0 in zgodila se bo napaka zaradi deljenja z 0 ob s / c.

Kako poskrbeti za te, različne, napake? Vrstico s += float(v) bi že lahko zaprli v en try-except, ampak vrstice for v in open(ime) pa ne moremo. To se ne da:

def poprecje(ime):
    s = 0
    c = 0
    try:  # Tole je neumnost, to ne gre tako!
        for v in open(ime):
    except:
        print("Datoteka {} ne obstaja".format(ime)
        s += float(v)
        c += 1
    return s / c

Ta s += float(v) in c += 1 sta zdaj končala v except. V try-except ne zapiramo vrstic, temveč cele bloke in for je blok, ki vključuje tudi vrstice, ki se ponavljajo, ne le glave zanke.

Vse skupaj bomo torej dali v en sam try-except. Nekako pa bomo morali razlikovati med vrstami napak. In zdaj pridejo na vrsto tisti ValueError in ZeroDivisionError in tako naprej. Kot smo povedali: ko se zgodi napaka, Python sestavi objekt, ki predstavlja napako. Ti objekti so različnih tipov, tako kot so različnih tipov napake. V except pa lahko povemo, kakšno izjemo želimo loviti. Doslej nam je except ulovil vse; če dodamo še vrsto napake, bo lovil samo to.

def poprecje(ime):
    s = 0
    c = 0
    try:
        for v in open(ime):
            s += float(v)
            c += 1
    except ValueError:
        print("Napaka pri pretvarjanju '{}' v število".format(v.strip()))
    return s / c

Tako napisan program bo prestregel napako pri pretvarjanju števila, ne pa tudi napako, ki se zgodi, če datoteka ne obstaja ali če je prazna. Če hočemo uloviti tudi tidve, dodamo še dva excepta.

def poprecje(ime):
    try:
        s = 0
        c = 0
        for v in open(ime):
            s += float(v)
            c += 1
        return s / c
    except IOError:
        print("Ne morem odpreti datoteke {}".format(ime))
    except ValueError:
        print("Napaka pri pretvarjanju '{}' v število".format(v.strip()))
    except ZeroDivisionError:
        print("Datoteka je prazna")

Se lahko zgodi še kaj? Morda. Hočemo uloviti? Morda.

def poprecje(ime):
    try:
        s = 0
        c = 0
        for v in open(ime):
            s += float(v)
            c += 1
        return s / c
    except IOError:
        print("Ne morem odpreti datoteke {}".format(ime))
    except ValueError:
        print("Napaka pri pretvarjanju '{}' v število".format(v.strip()))
    except ZeroDivisionError:
        print("Datoteka je prazna")
    except:
        print("Nepričakovana napaka")

Če na konec vseh exceptov dodamo še enega splošnega, bo ulovil vse, česar niso ulovili ostali.

Kje in kako loviti izjeme

Tule smo šli na zihr je zihr in v try-except zaprli kar celo funkcijo.

Lahko napišemo program tako, da bo preprosto preskočil vsa števila, ki jih ne more pretvoriti? Samo malo drugače ga moramo preobrniti.

def poprecje(ime):
    try:
        s = 0
        c = 0
        for v in open(ime):
            try:
                s += float(v)
                c += 1
            except ValueError:
                print("Napaka pri pretvarjanju '{}' v število".format(v.strip()))
        return s / c
    except IOError:
        print("Ne morem odpreti datoteke {}".format(ime))
    except ZeroDivisionError:
        print("Datoteka je prazna")
    except:
        print("Nepričakovana napaka")

To je po svoje lepše, saj napako lovimo tam, kjer jo pričakujemo, ne pa precej kasneje. Po drugi strani je seveda bolj zapleteno in raztreščeno.

Podobno bi lahko IOError lovili ob odpiranju datoteke. Prejle smo ugotovili, da v try-except ne moremo zapreti samo vrstice for. To je res. Vendar lahko datoteko odpremo že pred tem.

def poprecje(ime):
    try:
        s = 0
        c = 0
        try:
            podatki = open(ime)
        except IOError:
            print("Ne morem odpreti datoteke {}".format(ime))
            return
        for v in podatki:
            try:
                s += float(v)
                c += 1
            except ValueError:
                print("Napaka pri pretvarjanju '{}' v število".format(v.strip()))
        return s / c
    except ZeroDivisionError:
        print("Datoteka je prazna")
    except:
        print("Nepričakovana napaka")

Ne spreglejte, da smo po izpisu napake rekli return. Tule moramo prekiniti izvajanje funkcije, saj nimamo česa brati.

Zdaj pa še ZeroDivisionError. Tudi za tega točno vemo, kje se lahko zgodi.

def poprecje(ime):
    try:
        s = 0
        c = 0
        try:
            podatki = open(ime)
        except IOError:
            print("Ne morem odpreti datoteke {}".format(ime))
            return
        for v in podatki:
            try:
                s += float(v)
                c += 1
            except ValueError:
                print("Napaka pri pretvarjanju '{}' v število".format(v.strip()))
        try:
            return s / c
        except ZeroDivisionError:
            print("Datoteka je prazna")
    except:
        print("Nepričakovana napaka")

Kaj loviti?

Smo torej našli univerzalno zdravilo proti napakam v programu? Tule smo celotno funkcijo zaprli v try s povsem splošnim exceptom in, hura, nič več ne more iti narobe.

Takšne funkcije niso uporabne. Za začetek: sporočila o napakah naj izpisuje program, ne funkcije. Tale funkcija vedno, kadar gre kaj narobe, to izpiše in vrne None. Predstavljajte si, da bi funkcija float izpisala sporočilo (v angleščini), namesto da bi sprožila izjemo, in vrnila None. Tega ne bi bilo mogoče prestreči. A o sporočanju napak se bomo pogovorili kasneje.

Zdaj nas bolj žulji lovljenje. Kadar pokličemo kakšno funkcijo in ta svojega dela ne more opraviti, si želimo vedeti, zakaj - ne pa, da funkcija preprosto napiše "nekaj je narobe", mi pa naj ugibamo. Želimo vedeti kaj in kje, v kateri vrstici, da bomo lahko napako poiskali in popravili. Kar smo naredili v gornji funkciji je korak v napačno smer. Videti je prijazno do uporabnika, v resnici pa je neprijazno do programerja. Uporabnik pa dobi sporočila v slovenščini namesto nečesa malo bolj strašljivega v angleščini.

Predvsem pa tej stvari pravimo izjema, ne napaka. Uloviti hočemo stvari, ki lahko gredo narobe, ker nimamo kontrole nad njimi, ne pa pometati po preprogo napake, ki jih naredimo pri programiranju. Da, funkcija, ki prebere nek podatek z interneta, naj sproži izjemo, če internetna povezava ne deluje. Funkcija, ki bi utegnila nekje nekaj deliti z 0, pa naj pač pazi, s čim deli.

Znebimo se torej splošnega lovljenja in lovljenja deljenja z 0.

def poprecje(ime):
    s = 0
    c = 0
    try:
        podatki = open(ime)
    except IOError:
        print("Ne morem odpreti datoteke {}".format(ime))
        return
    for v in :
        try:
            s += float(v)
            c += 1
        except ValueError:
            print("Napaka pri pretvarjanju '{}' v število".format(v.strip()))
    if c > 0:
        return s / c
    print("Datoteka je prazna")

Tudi, ali datoteka obstaja, lahko preverimo, še preden jo poskusimo odpreti.

import os

def poprecje(ime):
    s = 0
    c = 0
    if os.path.exists(ime):
        podatki = open(ime)
    else:
        print("Ne morem odpreti datoteke {}".format(ime))
        return
    for v in :
        try:
            s += float(v)
            c += 1
        except ValueError:
            print("Napaka pri pretvarjanju '{}' v število".format(v.strip()))
    if c > 0:
        return s / c
    print("Datoteka je prazna")

Smo s tem, zadnjim, kaj pridobili? Niti ne; pravzaprav je lovljenje IOError čisto na mestu, saj ulovi tudi morebitne druge napake - na primer to, da datoteka obstaja, vendar je nimamo pravice brati.

Ostal nam je le še en try. Tega pa kar pustimo. Naj funkcija float sama pove, kaj zna in česa ne.

Kdaj bomo napisali if in kdaj try je pogosto stvar odločitve. Čeprav je try zanimiva igrača, priporočam, da z njeno uporabo ne pretiravate.

Argumenti izjem

Izjeme ne nosijo le vrste napake, temveč tudi argumente. "Vsebino" izjeme lahko spravimo v spremenljivko, takole:

        try:
            s += float(v)
            c += 1
        except ValueError as napaka:
            print("Napaka pri pretvarjanju '{}' v število".format(v.strip()))

Tule je napaka zdaj spremenljivka, ki je tipa ValueError in vsebuje še kaj več o napaki. Lahko jo, recimo, izpišemo.

        try:
            s += float(v)
            c += 1
        except ValueError as napaka:
            print(napaka)

Videli bomo, da se izpiše natančno tisto, kar bi izpisal Python, če te napake ne bi prestregli.

Tako kot pri zanki for, ki, kot smo se učili pred dvema tednoma, pravzaprav ne počne ničesar, le od generatorja vedno znova zahteva nov element, Python tudi pri izpisu napake ne naredi nič drugega, kot da reče napaki, naj se izpiše. Prej pa le še pove, kako je prišel do mesta, kjer se je napaka zgodila, tako da izpiše sklad, o katerem smo se učili na prvih dveh predavanjih.

Več o tej temi se ne bomo učili.

Zaključevanje

Bloku try lahko poleg enega ali več (ali nič!) exceptov sledita še dve stvari. Ena je else. Kar napišemo pod else se zgodi, če ni prišlo do izjeme.

V gornjem primeru bi lahko pisali

        try:
            s += float(v)
        except ValueError as napaka:
            print(napaka)
        else:
            c += 1

Stvar naredi popolnoma isto, le malo preglednejša je morda: pretvorimo število. Če ne gre, izpišemo napako, sicer si zabeležimo, da imamo še eno število več. Sam sicer ne vem, ali sem else za try sploh že kdaj uporabil...

Pač pa je veliko uporabnejši blok finally. Kar je v njem, se zgodi ne glede na to, ali je ob izvajanju prišlo do izjeme ali ne. Tudi tega za zdaj le omenimo; čeprav je tako uporaben, si zanj težko izmislim preprost primer, torej počakajmo na kak trenutek (letos ali drugo leto), ko nam bo v resnici prišel prav.

Sprožanje izjem

Zdaj pa drugi konec zgodbe. Izjeme mora tudi nekdo sprožati. Kako se naredi to?

Napišimo funkcijo za izračun ploščine trikotnika s stranicami a, b in c.

def ploscina(a, b, c):
    from math import sqrt
    s = (a + b + c) / 2
    p2 = s * (s - a) * (s - b) * (s - c)
    return sqrt(p2)

Tole ne deluje, če trikotnik ni mogoč. Če poskušamo izračunati ploščino trikotnika s stranicami 3, 4 in 10 (poskusite ga narisati!), bo število p2 negativno in ga ni mogoče koreniti. Recimo, da bi želela, da funkcija v tem primeru sproži napako.

def ploscina(a, b, c):
    from math import sqrt
    s = (a + b + c) / 2
    p2 = s * (s - a) * (s - b) * (s - c)
    if p2 < 0:
        raise ValueError("Trikotnik krši trikotniški neenakost")
    return sqrt(p2)

To je to.

Lahko smo tudi bolj eksplicitni.

def ploscina(a, b, c):
    from math import sqrt
    s = (a + b + c) / 2
    p2 = s * (s - a) * (s - b) * (s - c)
    if p2 < 0:
        raise ValueError(
            "Trikotnik ({}, {}, {}) krši trikotniški neenakost".format(a, b, c))
    return sqrt(p2)

Zakaj sem se odločil ravno za ValueError? Po opisu različnih vrst napak je za tole še najbolj primeren. Če vam kaj ni prav, pa si lahko izmislimo svojo izjemo.

class TriangleError(Exception):
    pass

def ploscina(a, b, c):
    from math import sqrt
    s = (a + b + c) / 2
    p2 = s * (s - a) * (s - b) * (s - c)
    if p2 < 0:
        raise TriangleError(
            "Trikotnik ({}, {}, {}) krši trikotniški neenakost".format(a, b, c))
    return sqrt(p2)

Izjeme morajo biti izpeljane iz Exception - ali iz katerega od njenih naslednikov. Morda, v tem primeru, iz ValueError.

class TriangleError(ValueError):
    pass

V definiciji TriangleError nimamo kaj povedati, zato smo rekli kar pass. No, v resnici bi lahko kaj imeli, vendar tudi v to tule ne bomo rinili.

마지막 수정됨: 수요일, 11 1월 2017, 12:53 PM