Uke 10 - Exceptions

Les gjennom kapittelet fra Pythons egen tutorial først: https://docs.python.org/3/tutorial/errors.html

Eksempler

Eksempel 1

En vanlig type feil når man bruker lister er IndexError. Det får man når man prøver å hente et element fra et index som er utenfor listen.

Her er et eksempel på dette. Prøv å kjøre denne koden:

xs = ['a', 'b', 'c']
print(xs[5]) # fails with IndexError

Vi kan håndtere feilet ved hjelp av try-except. Dette kan vi gjøre på noen forskjellige måter. Vi kan enten fange alle feil med å bare bruke except eller så kan vi bare fange IndexErrors ved å bruke except IndexError. Vi kan også fange både IndexErrors og TypeErrors ved å bruke except IndexError og except TypeError etter hverandre.

Her er eksempel på dette. Last ned koden her: eksempel_1.py og kjør den. Skjønner du forskjellen mellom de ulike måtene å bruke except?

xs = ['a', 'b', 'c']

# print(xs[5]) # fails with IndexError

#Option 1 - catch everything

try:
    x1 = xs[2]
    x2 = xs[5]
    print(x2 - x1)
except:
    print('Something went wrong')
    print('Decide what to do next here')
    # ...


#Option 2 - catch specific 

try:    
    x1 = xs[2]
    x2 = xs[5] # broken
    print(x2 - x1)
except IndexError as err:
    print('We caught an IndexError:', err)
    
#Option 3 - TypeError still goes out

try:    
    x1 = xs[2]
    x2 = xs[0]
    print(x2 - x1) # broken
except IndexError as err:
    print('We caught an IndexError:', err)

# get TypeError in Terminal


#Option 4 - both caught

try:    
    x1 = xs[2]
    x2 = xs[0]
    print(x2 - x1) # broken

except IndexError as err:
    print('We caught an IndexError:', err)

except TypeError as err:
    print('We caught a TypeError:', err)

Eksempel 2

I dette eksemplet skal vi lese inn tall fra en fil. Om det er noen feil i filen som blir leset inn kommer vi få en error i koden. Dette håndterer vi med try-except.

Last ned koden her: eksempel_2.py. Her fanger vi FileNotFoundError og ValueError for seg selv og så alle andre feil. Kan det bli noen andre feil enn FileNotFoundError og ValueError når vi bruker funksjonen get_numbers_from_file()?

def get_numbers_from_file(filename):
    """
    Read numbers from a file.
    The file format is a simple column like:

      31
      145
      -947

    If anything goes wrong, return an empty list
    """
    try:
        data = []
        with open(filename) as f:
            for line in f:
                number = int(line)
                data.append(number)
        return data

    except FileNotFoundError:
        print(f"Warning: {filename} does not exist.")
        return []

    except ValueError:
        print(f"Warning: {filename} contains items that are not integers.")
        return []

    except:
        print("Warning: Unknown problem")
        return []


# if __name__ == "__main__":
# make your own files and your own function calls to get_numbers_from_file() here

Prøv nå å lage dine egne filer og bruk funksjonen get_numbers_from_file() under if __name__ == "__main__":. Et kall av funksjonen skal være uten feil, et skal gi FileNotFoundError og et skal gi ValueError.

Eksempel 3

Det er ofte mulig å plassere try-except på ulike steder i koden sin. Da må man velge det stedet hvor det er mest naturlig at et feil blir håndtert.

Last ned filen her: eksempel_3_1.py. Funksjonen all_lines_through_points() skal printe ligningen for alle linjer mellom alle fire punkter den får som input (som en liste). Om det ikke er mulig å beregne ligningen mellom et par av punkter skal den printe hvorfor dette ikke er mulig og siden gå videre til neste par (programmet skal ikke krasje).

Om du prøver å kjøre koden så krasjer den. Plassere ut try-except hvor du synes det er mest naturlig, slik at koden fungerer som den skal. Hvorfor har du valgt å plassere try-except der du har plassert det?

def slope(x1, y1, x2, y2):
    return (y2 - y1) / (x2 - x1)


def y_intercept(x1, y1, x2, y2):
    return (x2 * y1 - x1 * y2) / (x2 - x1)


def line_eqn_from_points(p1, p2):
    # p1, p2 are 2-tuples
    x1, y1 = p1
    x2, y2 = p2

    a = slope(x1, y1, x2, y2)
    b = y_intercept(x1, y1, x2, y2)

    return f"y = {a}x + {b}"


def all_lines_through_points(points):
    num_points = len(points)
    for i in range(num_points):
        for j in range(i + 1, num_points):
            p1 = points[i]
            p2 = points[j]
            eq = line_eqn_from_points(p1, p2)
            print(f"The equation for the line between {p1} and {p2} is {eq}.")


if __name__ == "__main__":
    all_lines_through_points([
        (1, 1), 
        (-1, 1), 
        (1, -1), 
        (-1, 1),
    ])

Last ned filen her: eksempel_3_2.py som er samme kode som eksempel_3_1.py men med try-except, slik at koden ikke krasjer.

Her er try-except plassert i funksjonen all_lines_through_points(). Om try-except er i noen av funksjonene slope() og y_intercept() må vi si hva de funksjonene skal returnere om det ikke er mulig å beregne linjens stigning eller konstantledd. Her finnes det ingen selvfølgelig svar. Om funksjonene returnerer noen string med feilmelding må vi alltid sjekke hva vi får når vi bruker funksjonene og se om vi får et tall eller en feilmelding. Det er bedre om vi vet at vi alltid får et tall fra funksjonene, om de ikke krasjer.

Det er samme med å plassere try-except i line_eqn_from_points(). Da må vi si hva den funksjonen skal returnere om det blir en error. Når vi så bruker line_eqn_from_points() må vi alltid sjekke om strengen vi får er en feilmelding eller en ligning. Det er bedre å vite at all_lines_through_points() alltid returnerer en ligning, om den ikke krasjer.

Men i funksjonen all_lines_through_4_points() vet vi hva som skal skje om det ikke går å beregne ligningen for linjen mellom to punkter. Da skal vi printe en melding om dette og hvorfor det ikke er mulig. Derfor passer det bra å plassere try-except her.

Eksempel 4

Her er et eksempel hvor vi definerer en egen type error: NoSeat. Vi bruker den når det ikke finnes en ledig plass som oppfyller vilkåren.

Vi bruker også try except til å sjekke at input er som den skal. Ellers spør vi brukeren om ny input.

Last ned koden her: eksempel_4.py. Hvordan kan vi håndtere en plass som ikke er tilgjengelig uten å bruke exceptions? Synes du det er bedre å håndtere utilgjengelige seter med eller uten å bruke exceptions? Hvorfor?

class NoSeat(Exception):
    pass


def seat_from_position(chart, row, col):
    """
    Find out if seat at given position is available
    Update seating chart (in place)
    Return price of the seat
    """

    price = chart[row - 1][col - 1]

    if price == 0:
        raise NoSeat

    chart[row - 1][col - 1] = 0
    return price


def seat_from_price(chart, price):
    """
    Find available seat given a price
    Update seating chart (in place)
    Return seat position for an available seat of given price
    """

    for row, line in enumerate(chart):
        for col, seat in enumerate(line):
            if seat == price:
                chart[row][col] = 0
                return row + 1, col + 1

    raise NoSeat


def print_chart(chart):
    """
    Print seating chart
    """

    print()
    for line in chart:
        print(f"{line[0]:>2}", end="")
        for seat in line[1:]:
            print(f"{seat:>3}", end="")
        print()
    print()


def input_int_from_range(msg, min, max):
    while True:
        try:
            ans = input(msg)
            ans = int(ans)
            if min <= ans <= max:
                return ans
            else:
                print(f"{ans} is outside [{min},{max}]. Try again")
        except ValueError:
            print(f"Not an Integer: {ans}. Try again")


def ask_for_seat(chart):
    row = input_int_from_range("Please specify row number (1-9): ", 1, 9)
    col = input_int_from_range("Please specify column number (1-10): ", 1, 10)
    price = seat_from_position(chart, row, col)
    return price


def ask_for_price(chart):
    price = input_int_from_range("Please specify a price: ", 10, 50)
    row, col = seat_from_price(seating_chart, price)
    return row, col


if __name__ == "__main__":

    seating_chart = [
        [10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
        [10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
        [10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
        [10, 10, 20, 20, 20, 20, 20, 20, 10, 10],
        [10, 10, 20, 20, 20, 20, 20, 20, 10, 10],
        [10, 10, 20, 20, 20, 20, 20, 20, 10, 10],
        [20, 20, 30, 30, 40, 40, 30, 30, 20, 20],
        [20, 30, 30, 40, 50, 50, 40, 30, 30, 20],
        [30, 40, 50, 50, 50, 50, 50, 50, 40, 30],
    ]

    print("Welcome! Here you can purchase seats for the theatre.")
    print("Below are the prices for all the seats:")
    print_chart(seating_chart)
    print(
        """You can either specify a seat you would like to purchase,
or you can specify a price you want to pay for your seat.
You can buy as many seats as you like.\n"""
    )

    while True:

        print("Below is the current seating chart:")
        print_chart(seating_chart)

        option = input(
            "Type [s] to specify a seat, type [p] to specify a price or type [q] to quit: "
        )
        try:
            if option == "q":
                break
            elif option == "s":
                price = ask_for_seat(seating_chart)
                print(f'The seat costs kr.{price}.')
            elif option == "p":
                row, col = ask_for_price(seating_chart)
                print(f'You bought row {row}, seat {col}.')
            else:
                print("Please type either [s], [p] or [q].")

        except NoSeat:
            print("No free seats found that match your request, please try again.")