rivi.strip()

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 komennolla

luku = int("kolme")

Python on ilmoittanut ValueError-poikkeuksesta:

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. Napataan virheellisestä lukuarvosta johtuva ValueError-poikkeus:

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 (OSError)

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. Esimerkki:

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(f"Virheellinen lukuarvo {rivi.strip():s} tiedostossa {nimi:s}")
            
    # 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 tekstiä tiedostosta ja napataan poikkeukset (tässä tapauksessa tiedosto sisältää ainoastaan tekstin "keukeu"):

nimi = "teksti.txt"
try:
    # Yritetään avata tiedosto, tämä voi johtaa virheeseen
    tiedosto = open(nimi, "r")
    try:
        # Tiedosto aukesi onnistuneesti, yritetään lukea tiedoston sisältö read()-funktiolla
        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(f"Tiedostoa {nimi:s} ei voitu lukea")
    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 tekstin 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 (ks. seuraava luku).

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 (virhe avattaessa tai käsitellessä tiedostoa) ja ValueError (virhe muunnettaessa tekstiä lukuarvoksi). 

Monimutkaisemmissa ohjelmissa pitää ottaa huomioon myös erilaisten kirjastojen poikkeustilanteet. Oikeassa ohjelmistossa, jonka tehtävä on esimerkiksi valvoa kriittistä tuotantoprosessia, voikin olla enemmän virheenkäsittelykoodia kuin varsinaista toiminnallista koodia!

Tehtävä 5.5.1