Virhetilanteiden käsittely (try-except-finally)

Poikkeukset

Hyvässä ohjelmakoodissa varaudutaan aina erilaisiin virhetilanteisiin, kuten

  • Käyttäjän antama virheellinen syöte
  • Tiedoston avaaminen epäonnistuu

Pythonissa virhetilanteet hoidetaan poikkeusten (engl. exception) avulla.

Oletkin varmasti jo kohdannut lukuisia poikkeuksia kurssin aikana. Esimerkiksi jos olet yrittänyt muuntaa vääränlaista merkkijonoa lukuarvoksi:

luku = int("kolme")

Python on ilmoittanut ValueError-nimisestä poikkeuksesta (IPython-konsolin tuloste Spyderissä)

ValueError: invalid literal for int() with base 10: 'kolme'

Poikkeusten "nappaaminen" ohjelmakoodissa

Virhetilanteessa Python "nostaa" (engl. raise) poikkeuksen. Poikkeuksen voi "napata" (engl. catch) ja käsitellä, jolloin se ei johda ohjelman suorituksen keskeytymiseen.

Poikkeusten nappaamiseen ja käsittelemiseen käytetään try-except -rakennetta:

while True:
    try:
        luku = float(input("Anna liukuluku:\n"))
        # Jos suoritus jatkui tänne, käyttäjä antoi kelvollisen liukuluvun      
        break
    except ValueError:
        # Napataan ValueError-poikkeus (virheellinen lukuarvo)
        print("Virheellinen lukuarvo!")
        # Virhe on nyt käsitelty ja ohjelman suoritus palaa while-silmukan alkuun

print("Annoit luvun", luku)  

Esimerkkisuoritus:

Anna liukuluku:
> kolme piste kaksi
Virheellinen lukuarvo!

Anna liukuluku:
> 3.2
Annoit luvun 3.2

Poikkeus tiedostoa avattaessa

Tiedostoja käsitellessä voi tulla vastaan virhetilanteita (esimerkiksi yritetään avata tiedostoa, jota ei ole olemassa). Tällöin pitää napata virhe OSError:

# Luetaan rivit tiedostosta
try:
    tiedosto = open("rivit.txt", "r")
    for rivi in tiedosto:
        print(rivi)  
except OSError:
    print("Virhe avattaessa tiedostoa rivit.txt")

Sisäkkäiset try-lausekkeet

Monesti tarvitaan sisäkkäisiä try-lausekkeita, joilla hoidetaan erityyppiset virheet. Hyvä nyrkkisääntö on, että try-avainsana tulisi olla mahdollisimman lähellä riviä, jossa odotat virheen tapahtuvan (esimerkiksi alla).

nimi = "luku.txt"
try:
    # Yritetään avata tiedosto, tämä voi johtaa virheeseen
    tiedosto = open(nimi, "r")
    # Tiedosto aukesi onnistuneesti, luetaan siitä
    for rivi in tiedosto:
        try:
            # Yritetään muuntaa teksti liukuluvuksi
            luku = float(rivi)
            # Onnistui, tulostetaan luku
            print("Tiedosto sisälsi luvun", luku)  
        except ValueError:
            # float()-funktio aiheutti ValueError-virheen
            print("Virheellinen lukuarvo {:s} tiedostossa {:s}".format(rivi.strip(), nimi))
            
    # Suljetaan lopuksi tiedosto            
    tiedosto.close()
except OSError:
    # Tänne päädytään, jos open-funktio epäonnistui
    print("Virhe avattaessa tiedostoa", nimi)

Jos kaikki menee hyvin, ohjelma tulostaa:

Tiedosto sisälsi luvun 1.0

Jos tiedostoa ei ole olemassa, ohjelma tulostaa:

Virhe luettaessa tiedostoa luku.txt

Jos tiedostossa on virheellisiä lukuja, ohjelma tulostaa esimerkiksi

Virheellinen lukuarvo 1.0a tiedostossa luku.txt
Tiedosto sisälsi luvun 2.0

try-except-finally

try-except-finally-rakenteella voidaan varmistaa, että jokin asia tehdään varmasti, vaikka virheitä syntyisi. Luetaan lukuarvo tiedostosta ja napataan poikkeukset:

nimi = "teksti.txt"
try:
    # Yritetään avata tiedosto, tämä voi johtaa virheeseen
    tiedosto = open(nimi, "r")
    try:
        # Tiedosto aukesi onnistuneesti, yritetään lukea rivi
        teksti = tiedosto.read()
        # Onnistui, tulostetaan sisältö
        print("Tiedosto sisälsi tekstin", teksti)  
    except OSError:
        # read()-funktio aiheutti OSError-virheen, tiedostoa ei voi lukea
        print("Tiedostoa {:s} ei voitu lukea".format(nimi))
    finally: 
        # Suljetaan tiedosto riippumatta siitä, oliko virheitä vai ei
        print("Suljetaan tiedosto")        
        tiedosto.close()
except OSError:
    # Tänne päädytään, jos open-funktio epäonnistui.
    print("Virhe avattaessa tiedostoa", nimi)
    # Tiedostoa ei avattu, joten sitä ei tarvitse myöskään sulkea

Jos kaikki menee hyvin, ohjelma tulostaa:

Tiedosto sisälsi teksti keukeu
Suljetaan tiedosto

Jos tiedostoa ei ole olemassa, ohjelma tulostaa:

Virhe avattaessa tiedostoa teksti.txt

Jos tiedostossa on virheellisiä lukuja, ohjelma tulostaa

Tiedostoa teksti.txt ei voitu lukea
Suljetaan tiedosto

Ohjelma siis sulkee viimeisenä tekonaan tiedoston. Tämä on tärkeää ja tiedostojen kanssa tuleekin aina käyttää try-finally -rakennetta. Helpoiten tämän vaatimuksen voi kuitata käyttämällä with-lausetta.

Tiedostojen avaaminen with-lausekkeella

Pythonissa on kätevä with-lauseke, joka kutsuu automaattisesti close-funktiota ja näin try-finally-rakennetta ei tarvita. Luetaan tiedosto moolimassat.json käyttäen with-lauseketta:

with open("moolimassat.json", "r") as tiedosto:
    moolimassat = json.load(tiedosto)

with-lauseke korvaa siis seuraavan try-finally -rakenteen:

tiedosto = open("moolimassat.json", "r")
try:
    moolimassat = json.load(tiedosto)
finally:
    # Tämä osio suoritetaan aina
    tiedosto.close()

Koska myös open-funktion mahdolliset virheet on tärkeää käsitellä, with-lausekkeesta on parasta käyttää seuraavaa muotoa:

try:
    with open("moolimassat.json", "r") as tiedosto:
        moolimassat = json.load(tiedosto)
except OSError:
    print("Tiedoston avaaminen epaonnistui!")

Tämä viimeinen esimerkki on minimivaatimus virheenkäsittelylle tiedostoja avattaessa!

try-except-else(-finally)

try-except-rakenteeseen voi yhdistää myös else-osan, joka suoritetaan siinä tapauksessa, että try-osio ei aiheuttanut poikkeuksia:

try:
    luku = float(input("Anna luku:\n"))
except ValueError:
    print("Virheellinen luku")
else:
    print("Annoit luvun", luku)

try-except-else-rakenteeseen voi yhdistää vielä finally-osan, jossa esimerkiksi suljetaan avatut tiedostot.

Lista Pythonin poikkeuksista

Mistä tietää, mikä poikkeus pitäisi napata? Tässä on listä Pythonin sisäänrakennetuista poikkeuksista. Tällä kurssilla tärkeimmät poikkeukset ovat OSError ja ValueError. Monimutkaisemmissa ohjelmissa pitää ottaa huomioon erilaisten kirjastojen poikkeustilanteet. Oikeassa ohjelmistossa, jonka tehtävä on esimerkiksi valvoa kriittistä tuotantoprosessia, voikin olla enemmän virheenkäsittelykoodia kuin varsinaista toiminnallista koodia!