Obravnavanje in proženje izjem
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 except
a.
Č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
except
a. (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 a
ju, 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 except
a.
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 except
ov 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 except
om 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č!) except
ov 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.