Virhetilanteisiin varautuminen
Ohjelmointiin liittyvät virheet voidaan jakaa kahteen ryhmään:
- Syntaksivirheet, jotka estävät ohjelman suorittamisen kokonaan
- Suorituksen aikaiset virheet, jotka keskeyttävät ohjelman suorituksen
Ryhmän 1 virheet on yleensä helppoa korjata, koska Python-tulkki huomauttaa niistä, kun ohjelmaa yritetään suorittaa. Tällaisia virheitä ovat esimerkiksi puuttuva kaksoispiste silmukan alussa tai puuttuva lainausmerkki merkkijonon lopussa.
Ryhmän 2 virheet voivat olla hankalampia havaita, koska virhe voi tapahtua jossain vaiheessa ohjelman suorituksen aikana ja vain tietyissä tilanteissa. Ohjelma saattaa toimia yleensä hyvin mutta keskeytyä virheen takia jossain reunatapauksessa. Keskitymme seuraavaksi tällaisten virheiden käsittelyyn.
Syötteiden tarkastaminen
Usein virhetilanteet ohjelmien suorituksen aikana liittyvät jotenkin virheelliseen syötteeseen. Esimerkkejä virheellisistä syötteistä ovat
- puuttuvat tai tyhjät arvot: esimerkiksi pituus nolla tai tyhjä merkkijono nimenä
- negatiiviset arvot: esimerkiksi –15 reseptin aineosan painona
- puuttuva tai väärän niminen tiedosto
- liian pienet tai liian suuret arvot
- väärä indeksi (esim. viittaaminen indeksiin 3 merkkijonossa "moi")
- väärän tyyppiset arvot, esimerkiksi merkkijono luvun sijasta
Useimpiin virheistä voidaan onneksi varautua ohjelmallisesti. Tarkastellaan esimerkkinä ohjelmaa, joka lukee käyttäjältä syötteenä tämän iän ja testaa, että se on sallituissa rajoissa (vähintään 0 ja korkeintaan 150):
ika = int(input("Anna ikäsi: "))
if ika >= 0 and ika <= 150:
print("Ikä kelpaa")
else:
print("Virheellinen ikä")
Anna ikäsi: 25 Ikä kelpaa
Anna ikäsi: -3 Virheellinen ikä
Syötteen tarkastamisessa (eli validoinnissa) ilmenee kuitenkin puutteita, jos syötteeksi annetaan esimerkiksi merkkijono:
Anna ikäsi: kakskytkolme ValueError: invalid literal for int() with base 10: 'kakskytkolme'
Virhe johtuu siitä, että funktio int
ei pysty muuttamaan merkkijonoa kakskytkolme
kokonaisluvuksi. Tämän seurauksena ohjelman suoritus keskeytyy yllä olevaan virheilmoitukseen.
Poikkeukset
Ohjelman suorituksen aikaisia virheitä kutsutaan poikkeuksiksi (exception). Ohjelman koodissa on mahdollista varautua poikkeuksiin ja käsitellä ne ilman, että ohjelman suoritus keskeytyy.
Pythonissa poikkeukset käsitellään try
- ja except
-lauseilla. Ideana on, että mikäli try
-lohkossa tapahtuu jokin poikkeus, Python tarkistaa, onko tälle poikkeukselle määritelty except
-lohkoa. Mikäli on, suoritetaan tämä lohko ja suoritus jatkuu sen jälkeen normaalisti.
Muutetaan edellä esitettyä esimerkkiä siten, että ohjelma varautuu poikkeukseen ValueError
:
try:
ika = int(input("Anna ikäsi: "))
except ValueError:
ika = -1
if ika >= 0 and ika <= 150:
print("Ikä kelpaa")
else:
print("Virheellinen ikä")
Anna ikäsi: kakskytkolme Virheellinen ikä
Ohjelmassa voidaan siis try
-lauseella ilmoittaa, että seuraavan lohkon sisällä tapahtuva toiminta voi aiheuttaa virheen. Välittömästi try
-lohkoa seuraavassa except
-lauseessa ilmoitetaan, mihin virheeseen varaudutaan. Edellisessä esimerkissä varauduttiin ainoastaan virheeseen ValueError
- jokin muu virhe olisi edelleen katkaissut ohjelman suorituksen.
Tässä tapauksessa virhetilanteessa muuttuja ika
saa arvon -1, jolloin ohjelma tunnistaa oikein virheellisen iän, koska ehtona on, että ikä on vähintään 0.
Seuraava funktio lue_kokonaisluku
lukee käyttäjältä kokonaisluvun varautuen siihen, että käyttäjä antaa virheellisen syötteen. Funktio kysyy lukua uudestaan niin kauan, kunnes käyttäjä lopulta antaa kelvollisen luvun.
def lue_kokonaisluku():
while True:
try:
syote = input("Syötä kokonaisluku: ")
return int(syote)
except ValueError:
print("Virheellinen syöte")
luku = lue_kokonaisluku()
print("Kiitos!")
print(luku, "potenssiin kolme on", luku**3)
Syötä kokonaisluku: kolme Virheellinen syöte Syötä kokonaisluku: aybabtu Virheellinen syöte Syötä kokonaisluku: 5 Kiitos! 5 potenssiin kolme on 125
Joissain tilanteissa saattaa olla tarvetta varautua poikkeukseen, mutta poikkeuksen tapahtuessa riittää "ignoorata" se, eli jättää koko asia huomiomatta except
-lohkossa.
Jos muuttaisimme edellistä esimerkkiä siten, että funktio hyväksyisi ainoastaan lukua 100 pienemmät kokonaisluvut, voisimme muuttaa toteutusta seuraavasti:
def lue_pieni_kokonaisluku():
while True:
try:
syote = input("Syötä kokonaisluku: ")
luku = int(syote)
if luku < 100:
return luku
except ValueError:
pass # tämä komento ei tee mitään
print("Virheellinen syöte")
luku = lue_pieni_kokonaisluku()
print(luku, "potenssiin kolme on", luku**3)
Syötä kokonaisluku: kolme Virheellinen syöte Syötä kokonaisluku: 1000 Virheellinen syöte Syötä kokonaisluku: 5 Kiitos! 5 potenssiin kolme on 125
Nyt siis poikkeuksen käsittelevässä lohkossa on ainoastaan komento pass
, joka ei tee mitään. Komento tarvitaan, sillä Python ei salli tyhjiä lohkoja.
Tyypillisiä virheitä
Seuraavassa on listattu joitakin yleisiä virheitä ja syitä niiden ilmenemiselle:
ValueError
Tämä poikkeus voi johtua siitä, että funktion parametri on vääränlainen. Esimerkiksi kutsu float("1,23")
tuottaa tämän poikkeuksen, koska Pythonissa desimaalierottimen tulee olla piste eikä pilkku.
TypeError
Tämä poikkeus tapahtuu, kun arvo on väärän tyyppinen. Esimerkiksi kutsu len(10)
saa aikaan tämän poikkeuksen, koska funktio len
haluaa parametrin, jolle voidaan laskea pituus (kuten merkkijonon tai listan).
IndexError
Tämä poikkeus tapahtuu, jos yritetään viitata indeksiin, jota ei ole olemassa. Esimerkiksi "abc"[5]
aiheuttaa tämän poikkeuksen, koska merkkijonossa ei ole indeksiä 5.
ZeroDivisionError
Tämä poikkeus tapahtuu, jos yritetään jakaa nollalla. Yksi esimerkki on tilanne, jossa yritetään laskea listan arvojen keskiarvo kaavalla sum(lista) / len(lista)
, mutta listan pituus on nolla.
Tiedostojen poikkeukset
Tiedostojen käsittelyssä voi tulla vastaan esimerkiksi poikkeukset FileNotFoundError (koetetaan lukea tiedostoa, jota ei ole olemassa), io.UnsupportedOperation (tiedosto on avattu tilassa, joka ei salli operaatiota) tai PermissionError (ohjelmalla ei ole oikeutta käsitellä tiedostoa).
Useamman poikkeuksen käsittely
Yhtä try
-lohkoa kohti voi olla useampia except
-lauseita. Esimerkiksi seuraavassa ohjelmassa varaudutaan sekä poikkeukseen FileNotFoundException
että PermissionError
:
try:
with open("esimerkki.txt") as tiedosto:
for rivi in tiedosto:
print(rivi)
except FileNotFoundError:
print("Tiedostoa esimerkki.txt ei löytynyt")
except PermissionError:
print("Ei oikeutta avata tiedostoa esimerkki.txt")
Aina ei ole tarpeen eritellä tapahtuneita virheitä. Esimerkiksi juuri tiedostoa avatessa saattaa riittää, että tiedetään virheen tapahtuneen, muttei ole niin tärkeää tietää, miksi virhe tapahtui. Kaikki mahdolliset virheet voi käsitellä käyttämällä except
-lausetta määrittelemättä poikkeuksen tyyppiä:
try:
with open("esimerkki.txt") as tiedosto:
for rivi in tiedosto:
print(rivi)
except:
print("Tapahtui virhe tiedoston lukemisessa")
Huomaa, että tällaisessa tapauksessa except
-lause käsittelee kaikki mahdolliset virheet, myös ohjelmoijan tekemät virheet lukuun ottamatta syntaksivirheitä, jotka estävät ohjelman suorittamisen.
Esimerkiksi seuraava ohjelma heittää aina poikkeuksen, koska muuttujan tiedosto
nimi on kirjoitettu toisessa kohdassa väärin tiedotso
.
try:
with open("esimerkki.txt") as tiedosto:
for rivi in tiedotso:
print(rivi)
except:
print("Tapahtui virhe tiedoston lukemisessa.")
Tästä näkee, että except
voi peittää varsinaisen virheen: tässä tapauksessa virheen syynä ei ole tiedoston käsittely vaan väärin kirjoitettu muuttuja.
Poikkeusten välittyminen
Jos funktion sisällä tapahtuu poikkeus, jota ei käsitellä, poikkeus välitetään funktion kutsujalle. Tätä jatketaan, kunnes ollaan pääohjelman tasolla. Jos poikkeusta ei tässäkään käsitellä sopivalla except
-lauseella, ohjelman suoritus katkeaa ja poikkeus yleensä tulostetaan ruudulle.
Esimerkiksi seuraavassa ohjelmassa funktiossa testi
tapahtuva poikkeus käsitellään vasta pääohjelmassa:
def testi(x):
print(int(x) + 1)
try:
luku = input("Anna luku: ")
testi(luku)
except:
print("Jotain meni pieleen")
Anna luku: kolme Jotain meni pieleen
Poikkeusten tuottaminen
Voimme myös tarvittaessa tuottaa poikkeuksen itse komennolla raise
. Vaikka virheiden tuottaminen varta vasten voi aluksi tuntua oudolta ajatukselta, mekanismi on itse asiassa hyvinkin hyödyllinen.
Esimerkiksi jos teemme funktion, jolle annetaan virheellinen parametri, voimme ilmaista tämän poikkeuksen avulla. Tämä voi olla parempi tapa kuin esimerkiksi palauttaa jokin virhearvo tai tulostaa viesti ruudulle, koska funktion käyttäjä ei välttämättä huomaisi asiaa.
Seuraavassa esimerkissä funktio kertoma
laskee parametrina annetun luvun kertoman (esimerkiksi luvun 5 kertoma on 1 * 2 * 3 * 4 * 5). Kuitenkin jos annettu luku on negatiivinen, funktio tuottaa poikkeuksen.
def kertoma(n):
if n < 0:
raise ValueError("Negatiivinen syöte: " + str(n))
k = 1
for i in range(2, n + 1):
k *= i
return k
print(kertoma(3))
print(kertoma(6))
print(kertoma(-1))
Log in to view the quiz