Erindid (Exceptions)

Mõnikord koodi kompileerimisel ja/või käivitamisel võivad tekkida erindid. 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'

Erind (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 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.

Erindid takistavad programmi tööd ja seega peavad olema ennetatavad. Ennetatud erindid võimaldavad vältida programmi töötamise ootamatut lõppemist. Ennetamine tähendab seda, et koodi kirjutamisel määratakse programmis sellised ohtlikud kohad, mis võivad visata vigu, ja üritavad neid “kinni püüda”.

Seda “kinni püüdmist” nimetatakse erinditega tegelemiseks (exception handling). Erinditega tegelemiseks kasutatakse kahte koodiplokki, mille võtmesõnadeks on try ja except. Konstruktsioonile võib ka lisada finally ploki, aga see ei ole kohustuslik.

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

Üleval olev näide on siis ümber kirjutatav kujul

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")

Veel mõni näide. 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 sees olevate arvude summat.

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 väljundiks

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

Kus 79 on kõikide järjendis olevate arvude summa.

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.

Olgu näiteks meil on järjend erinevate mängude nimedega. Meie tahame koostada programmi, mis küsib kasutajalt arvu ja tagastab mängu vastava indeksiga järjendist. Kõige lihtsam implementatsioon on järgmine:

games = ["Life is Strange", "The Cat lady", "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}'")  # Print the result to the console

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 ei ole hea programm ja programmi tuleb parandada. Antud juhul siis võib tekkida kaks erinevat viga: kui kasutaja ei sisesta arvu või kui sisestatud arv on liiga suur/väike. Proovime oma programmi parandada.

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

games = ["Life is Strange", "The Cat lady", "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}'")  # Print the result to the console

Antud näide korral see ei ole veel nii hull, aga isegi sellel juhul on koodi üsna palju ja seda on keeruline lugeda. 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. Näiteks teine tingimuslause iseloomustab ühte erindi klassi - IndexError -, ja on asendatav lausega except IndexError, mis on palju arusaadavam koodi lugejale.

games = ["Life is Strange", "The Cat lady", "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}'")

# If the input value is not a number
except ValueError:
    print("This is not even a number!")

# If the input number is not a valid index
except IndexError:
    print("This is not a valid number!")

Nüüd meie programm on palju paindlikum ja ilusam, kuigi kasutajale on kohe näha, mis mängu ta valib. Ei ole huvitav. Teeme nii, et iga kord koos indeksi küsimisega meie järjend uueneb ja igal elemendil on nüüd juhuslik indeks. Lisame juurde ka while-tsükli, et inimene saaks mitu korda valida endale mängu.

import random

games = ["Life is Strange", "The Cat Lady", "Soma", "Heavy Rain", "Fahrenheit", "Witcher 3", "Skyrim"]

# While the user wants to continue
while True:

    # Shuffle the initial list in random order
    random.shuffle(games)

    # Ask for input
    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}'")

        if game != "Life is Strange":
            print("But definitely check out 'Life is Strange'. Masterpiece!")

        # If the initial input is correct, ask the user if he wants to try again
        # If the user input equals yes, nothing happens and the loop is executed again
        if input("Wanna try again? (type 'yes'!) ") != "yes":
            # If the user input is anything but yes we break out of the loop
            break

    except ValueError:
        print("This is not even a number!")

    except IndexError:
        print("This is not a valid index!")

Erindite tõstatamine (Raising exceptions)

Üleval oli juba toodud üks näide erindi tõstatamisest: raise SystemExit, mis lõpetab kohe programmi tööd. 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

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

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

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

Kui user_input on näiteks 12, saame

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!

Erindi tõstatamine programmitöö juhtimiseks ja kasutaja loodud erinditüübid

Kujutame ette, et meil on robot, millel on funktsioon move(direction), mis tagastab roboti lõpp-positsiooni. Seni, kuni robot saab vabalt liikuda, ei ole probleemi. Kui nüüd aga robot mingil põhjusel ei saa liikuda, siis robot tagastab sama lõpp-positsiooni, kust ta alustas. See aga võib juhtuda mitmel erineval põhjusel. Üks variant oleks teha nii, et mõnikord tagastab funktsioon näiteks enniku x, y, teinekord aga sõne, näiteks The wall is ahead või My motor is not working vms. Kuigi selline asi põhimõtteliselt töötab Pythonis, ei ole see kuigi mõistlik lahendus. Pigem kasutatakse võimalust tõsta erind.

Üks variant on kasutada olemasolevaid erindiklasse. Näiteks ValueError. Juhul, kui ette antud direction väärtus ei ole lubatud, siis saab kirjutada:

def move(direction):
    if direction not in allowed_directions:
        raise ValueError(f"Incorrect direction use one of: {allowed_directions}")

Täiendavalt on võimalus kasutada kasutaja loodud erinditüüpe.

Kasutaja loodud erinditüübid

Erind on objekt. Objekte saab laiendada. Sama kehtib ka erindite puhul. Oma erindi loomiseks võime kirjutada nii:

class RobotInvalidInputException(Exception):
    pass

Siin luuakse uut tüüpi erind RobotInvalidInputException, see laiendab Exception klassi. Loodud uuele tüübile me mingit sisu lisada ei taha (ta päris kõik Exception klassi meetodid ja muutujad). Kuna Pythonis me klassi kirjeldust tühjaks ei saa jätta, lisame sinna pass.

Loome teise erindi riistvara probleemide jaoks:

class RobotHardwareException(Exception):
    def __init__(self, message, hardware_part):
        super().__init__(message)
        self.hardware_part = hardware_part

    def get_hardware_part(self):
        return self.hardware_part

Siin samamoodi loome uue klassi (tüübi) RobotHardwareException laiendades Exception klassi. Täiendame olemasolevat konstruktorit sedasi, et alati tuleb koos teatega kaasa anda riistvara komponendi kood. See kood salvestatakse objekti muutujasse `self.hardware_part. Hiljem on võimalik küsida, millise komponendiga probleem tekkis.

Siin on näide kasutamisest:

def move(direction):
    if direction not in allowed_directions:
        raise RobotInvalidInputException(f"Incorrect direction use one of: {allowed_directions}")

    if not motor_is_working():
        raise RobotHardwareException("Motor is not working", "Motor RR-102")


def operate():
    try:
        move("U")
    except RobotHardwareException as ex:
        print("Cannot move due to hardware problem")
        print(f"Malfunctioning part: {ex.get_hardware_part()}")