Erindid (Exceptions)

Mõnikord koodi kompileerimisel ja/või käivitamisel võivad tekkida erinevad veateated. Oluline on nendest vigadest aru saada ning teada, kuidas sellisel juhul toimida.

Erind (ingl k exception) on mingi eriolukord (mitte tingimata viga), mis programmi töö jooksul ilmneb. Üldjuhul programmi töö seiskub erindi tõttu. Selleks, et programmi töö ootamatult ei lõppeks, tuleb erindeid töödelda, ehk programmile öelda, mida erindi puhul ette võtta.

Erind võib tekkida näiteks siis, kui proovime konverteerida sõne numbriks:

some_word = "word"
int(some_word)  # --> ValueError: invalid literal for int() with base 10: 'word'

Erindite töötlemine

Erindeid on võimalik "kinni püüda" ja määrata programmi edasine käitumine. Seda "kinni püüdmist" nimetatakse erinditega tegelemiseks (ingl k exception handling). Erinditega tegelemiseks kasutatakse kahte koodiplokki, mille võtmesõnadeks on try ja except. Lausesse try lisatakse kood, mis võib tekitada viga. Vea tekkimisel käivitatakse tgevus, mis on kirjutatud except lausesse. Juhul kui viga ei teki, siis täidetakse try osas kogu kood. Konstruktsioonile võib ka lisada finally ploki, aga see ei ole kohustuslik.

Erindi kinni püüdmise struktuur:

try:
    # Try to to something, which may cause an error

except <Name of the error class>
    # If what you tried caused an error, this code block is executed
    # If the error was not thrown, this code block is ignored

finally:
    # This code block is executed anyway

Ülalolev näide on ümberkirjutatav järgnevale kujule:

some_word = "word"

try:
    # The ValueError is being thrown then executing this statement
    int(some_word)
    # After the error is thrown, the <except> code block is immediately executed
    print("This is not printed to the console")

# Catching the thrown error
except ValueError:
    print("This is printed to the console")

finally:
    print("This is printed anyway")

Vaatame veel mõnd näidet - oletame, et meil on olemas järjend, mille sees võivad olla täiesti juhuslikud elemendid (arvud, sõned, teised järjendid jne). Meie tahame arvutada selle järjendi sees olevate arvude summat nii, et kui järjendi elemendiks on mõni teine andmetüüp, ei lähe programm katki, vaid trükitakse konsooli vastavasisuline info.

black_box = ["fly", 42, 13, "bird", ["sub", "list"], "dinosaur", 24]

sum = 0

for element in black_box:
    try:
        # Try to add element to the sum
        sum += element

    except TypeError:
        # If it is not a number, print the message and continue the loop
        print(f"'{element}' is not a number!")

print(sum)

Saame koodi käivitamisega järgneva tulemuse, kus 79 on kõikide järjendis olevate arvude summa:

'fly' is not a number!
'bird' is not a number!
'['sub', 'list']' is not a number!
'dinosaur' is not a number!
79

Erindite kinni püüdmine võimaldab lahendada selliseid määramatust sisalduvaid probleeme väga elegantselt. Erindite kinni püüdmine on asendamatu näiteks kasutaja sisendi töötlemisel.

Vaatame veel üht näidet: olgu meil järjend erinevate mängude nimedega. Me tahame luua programmi, mis küsib kasutajalt arvu ja tagastab vastava indeksiga mängu järjendist. Kõige lihtsam implementatsioon on järgmine:

games = ["Minecraft", "GTA V", "Soma", "Heavy Rain", "Fahrenheit", "Witcher 3", "Skyrim"]

input_index = input(f"Type the number (0-{len(games) - 1}) to get a game to play: ")  # Ask for input number

game = games[int(input_index)]  # Get the element from list with the input index

print(f"And you should play '{game}'")

See on nö straight-forward lahendus, mis ei ole eriti paindlik. Kui kasutaja sisestab sõne, tühiku või üldse midagi muud, mis ei ole arv, läheb meie programm katki. Samamoodi läheb ta katki siis, kui sisestatud arv ei sobi indeksiks (on liiga suur või väike). Ja kui eksisteerib olukord, mille korral meie programm läheb katki, siis see kood ei ole hea ja seda programmi tuleks parandada. Antud näite puhul võivad tekkida kaks erinevat viga: kui kasutaja ei sisesta arvu või kui sisestatud arv on liiga suur/väike.

Kui me prooviksime teha vigade ennustamist ilma erindite kinni püüdmiseta, siis peaksime iga veaohtlikku olukorda eraldi vaatlema.

games = ["Minecraft", "GTA V", "Soma", "Heavy Rain", "Fahrenheit", "Witcher 3", "Skyrim"]

input_index = input(f"Type the number (0-{len(games) - 1}) to get a game to play: ")  # Ask for input number

# If input value is not a number
if not input_index.isdigit():
    print("This is not a number!")
    raise SystemExit  # Need to raise SystemExit for our program to stop

# If the input value is not a valid index
if int(input_index) > len(games) - 1 or int(input_index) < 0:
    print("This is not a valid index!")
    raise SystemExit

# If we have not raised SystemExit before, we will face the error anyway
game = games[int(input_index)]  # Get the element from list with the input index

print(f"And you should play '{game}'")

Tingimuslause kasutamine antud juhul eeldab, et me määrame iga võimaliku olukorra, mille korral meie programm võib minna katki. Kuna neid olukordi võib olla väga palju, siis see lahendus ei ole eriti hea, koodi võib tekkida ülemäära palju ning seda on keeruline lugeda. Näiteks teine tingimuslause iseloomustab ühte konkreetset erindi klassi (IndexError) ja on asendatav lausega except IndexError, mis on palju arusaadavam.

Kirjutame koodi ümber, kasutades erindite püüdmiseks try-except plokki:

games = ["Minecraft", "GTA V", "Soma", "Heavy Rain", "Fahrenheit", "Witcher 3", "Skyrim"]

input_index = input(f"Type the number (0-{len(games) - 1}) to get a game to play: ")

try:
    game = games[int(input_index)]
    print(f"And you should play '{game}'")
except ValueError:
    print("This is not a number!") # If the input value is not a number
except IndexError:
    print("This is not a valid number!") # If the input number is not a valid index

Nüüd on meie kood palju paindlikum ja ilusam, ühtlasi on väiksem tõenäosus, et koodi käivitamisena tekib programmi töös viga.

Erindite tõstatamine programmitöö juhtimiseks

Üleval oli juba toodud üks näide erindi tõstatamisest: raise SystemExit, mis lõpetab koheselt programmi töö. Erindite tõstatamine on kasulik siis, kui mingil konkreetsel juhul tuleb kohe programmi töö lõpetada ja näidata kasutajale vea põhjust ja kohta.

Näiteks on meil kood, kus küsime kasutajalt sisendiks sõna, kuid tahame tagastada vea, kui sisendiks on number:

user_input = input("Please input a word: ")

# If the input is a number
if word.isdigit():
    raise ValueError("This is not a word!") # Throw an error with specified message

# This will never be printed in case of error
print(f"The word is '{user_input}'")

Kui kasutaja poolt sisestatud user_input on näiteks 12, saame tulemuseks:

Traceback (most recent call last):
  File "C:/Users/user/PycharmProjects/project/ex.py", line 4, in <module>
    raise ValueError("This is not a word!")
ValueError: This is not a word!

Kasutaja loodud erinditüübid

Erind on objekt, mis kannab endaga kaasas täiendavat infot:

  • klassi nimi (tavaliselt annab see juba infot, mis probleem võis tekkida, nt IndexError)

  • erindi teade (message, tekkepõhjus, täpsustus vms)

  • erindi tekkimise koht (fail, rida)

  • kogu eelnenud programmi kutsungite ahel (mis meetod kutsus välja meetodi, kus erind tekkis jne), ingl k stack trace.

Vaatame üht näidet, kus kasutaja loodud erinditüübid aitavad tõstatada ja püüda vigu.

Alljärgnev programm palub kasutajal sisestada numbreid, kuni nad arvavad eelnevalt salvestatud numbri õigesti ära. Selleks, et kasutajate elu lihtsamaks teha, tagastatakse vihje, kas pakutud number on suurem või väiksem kui salvestatud number.

 1    # Define Python user-defined exceptions
 2    class Error(Exception):
 3        """Base class for other exceptions"""
 4        pass
 5
 6
 7    class ValueTooSmallError(Error):
 8        """Raised when the input value is too small"""
 9        pass
10
11
12    class ValueTooLargeError(Error):
13        """Raised when the input value is too large"""
14        pass
15
16
17    # You need to guess this number
18    number = 10
19
20    # User guesses a number until he/she gets it right
21    while True:
22        try:
23            i_num = int(input("Enter a number: "))
24            if i_num < number:
25                raise ValueTooSmallError
26            elif i_num > number:
27                raise ValueTooLargeError
28            break
29        except ValueTooSmallError:
30            print("This value is too small, try again!")
31        except ValueTooLargeError:
32            print("This value is too large, try again!")
33
34    print("Congratulations! You guessed it correctly.")

Allpool on toodud võimalik väljund:

Enter a number: 12
This value is too large, try again!

Enter a number: 0
This value is too small, try again!

Enter a number: 8
This value is too small, try again!

Enter a number: 10
Congratulations! You guessed it correctly.

Nii luuakse uus alamklass Error (mis laiendab sisseehitatud Exception klassi) ning uut tüüpi erindid ValueTooSmallError ja ValueTooLargeError, mis laiendavad meie loodud Error klassi. Loodud uuele tüübile me mingit sisu lisada ei taha (ta pärib kõik Exception klassi meetodid ja muutujad). Kuna Pythonis me klassi kirjeldust tühjaks ei saa jätta, lisame sinna võtmesõna pass.

See on tavapärane viis Pythoni programmeerimiskeeles määratleda kasutaja poolt loodud erindeid, kuid neid võimalusi on veelgi (lisada argumente, sõnumeid ja muud).