Göm menyn

Undantag

Ett undantag (eng. exception) är en extraordinär mer eller mindre oförutsägbar händelse som gör att ett program inte har möjlighet att fortsätta köras.

Grundidén med undantag är att kunna hantera olika typer av fel eller problem som kan inträffa, men som inträffar sällan, till exempel:

  • Filen man försökte öppna fanns inte
  • Katalogen där man försökte skriva en fil var skrivskyddad
  • Minnet tog slut
  • En server på nätverket svarar inte
  • Programmet var felskrivet och försökte komma åt seq[2] i en tom lista seq
  • ...

Felhantering utan undantag

Många sådana fel skulle man kunna upptäcka med hjälp av speciella returvärden, vilket var det sätt man ofta använde i äldre språk:

file = open("some-file.txt")
if file is None:
    # Felhanteringskod
else:
    # Filen gick att öppna
    indata = file.read() 

Men då blir det en hel del felhanteringskod som blandas in mitt i den "vanliga" koden. Dessutom måste man ju veta hur felen signaleras (vilka returvärden som signalerar felaktigheter) och komma ihåg att se efter om det blev fel eller inte. Det kan man lätt glömma:

file = open("some-file.txt")
# Vadå, kan det här misslyckas???
indata = file.read() 

Och om man glömmer att hela tiden se efter om det fungerade eller inte, kan det hända att felen inte upptäcks där de inträffar utan långt senare, som mer eller mindre oförklarliga följdfel där det kan ta lång tid att hitta orsaken:

image = read_image(filename)
# ... Returnerar None för att bildfilen inte finns, men det märker vi ju inte 
# ... Skickar vidare image via 10 nivåer funktioner ...
# ... Sedan lagras image någonstans ...
# ... Sedan händer andra saker ...
# ... Sedan kommer vi till slut till:
paint_image_on_screen(image)
# Krasch: "'NoneType' object is not subscriptable"
# Var kom det ifrån??? 

Felhantering med undantag

Undantagshantering ger ett standardiserat sätt att hantera de fel och problem som kan uppstå i undantagsfall. Med hjälp av speciella kontrollstrukturer (try / except) kan man i Python se tydligt vilken del av koden som är felhantering och vilken del som gäller "normalfallet":

try:
    file = open("some-file.txt")
    # Om vi kommer hit gick filen att öppna
    indata = file.read() 
except FileNotFoundError:
    # Felhanteringskod

Som man kan se ovan behöver vi dessutom inte stoppa in tester (if-satser) mitt inuti den kod som körs i normalfallet, när filen faktiskt gick att öppna. Istället kan den koden öppna filen, läsa från den, och gå vidare utan avbrott av feltester. Felkontrollen ligger så att säga utanför: Den "vanliga" koden är omringad av felkontrollen. Detta kan göra koden lättare att läsa, speciellt när det finns många olika fel som kan uppstå i ett litet kodavsnitt.

Men kan man inte fortfarande glömma att ta hand om felen? Jo, men då kommer Python inte bara att köra vidare. Om vi bara skriver följande...

file = open("some-file.txt")
indata = file.read() 

...och filen inte finns, så kommer Python inte att gå vidare med file.read() utan kommer att avbryta kodkörningen och visa upp ett FileNotFoundError för användaren. Man har alltså en mindre risk att få underliga följdfel där man inte vet vad som gick fel, eller var det gick fel. Istället får man alltså ett felmeddelande som talar om att det faktiska felet var att filen inte hittades, och som dessutom talar om var detta inträffade.

Nedan ser vi ett exempel på vad som händer när ett fel inte har en felhanterare (alltså när man inte fångar upp felet med "try/except"):

>>> 42 / 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

Anropsstacken (traceback) visar vilken kod som kördes innan felet uppstod. I detta fall finns bara en enda rad, men om man kör ett större program kan man få många rader som visar vilka funktioner som har anropats.

Sista raden visar vilket undantag (typ av fel) som har inträffat samt ett felmeddelande.

Dokumentation för undantag i Python

Några vanliga typer av undantag

Undantagen i tabellen nedan handlar främst om programmeringsfel. Även dessa hanteras alltså som undantag.

Exempelkod Undantag Felmeddelande
a NameError name 'a' is not defined
b[42] IndexError list index out of range
"abc"[2]='q' TypeError 'str' object does not support item assignment
$!#& SyntaxError invalid syntax
2/0 ZeroDivisionError division by zero
int("abc") ValueError invalid literal for int() with base 10: 'abc'

Det finns ett sextiotal olika undantagstyper i Python. Vi kan också skapa egna undantagstyper, men det kommer vi inte att gå igenom i den här kursen.

Exempel utan felkontroller

def find_root():
    """ Finds the square root of a number entered by the user. """
    x = eval(input("Enter a number: "))
    guess = x / 2
    for i in range(5):
        guess = (guess + x / guess)/2
        print(guess)

if __name__ == "__main__":
    find_root()

Sista if-satsen ser till att find_root() anropas när vi kör filen, men inte när vi importerar den.

Exempel med felkontroller

Här kontrollerar vi själva den input vi får frpn en användare, för att i förväg vara säkra på att vi har korrekta indata för funktioner som int(input_str):

def find_root():
    """ Finds the square root of a number entered by the user. """
    input_str = input("Enter a number: ")
    if not input_str.isnumeric():
        print("You must enter a number!")
        return
    x = int(input_str)
    if x == 0:
        print("You must enter a non-zero number!")
        return
    guess = x / 2
    for i in range(5):
        guess = (guess + x / guess)/2
        print(guess)

if __name__ == "__main__":
    find_root()

Exempel med undantagshantering

Istället för att kontrollera om värdet är rätt så kan vi försöka att köra koden. Ifall något går fel i koden kommer ett undantag att kastas, och då kan vi hantera det på ett separat ställe med hjälp try-catch.

def find_root():
    """ Finds the square root of a number entered by the user. """
    try:
        x = eval(input("Enter a number: "))
        guess = x / 2
        for i in range(5):
            guess = (guess + x / guess)/2
            print(guess)
    except ValueError:
        print("You must enter a number!")
    except ZeroDivisionError:
        print("You must enter a non-zero number!")

if __name__ == "__main__":
    find_root()

Olika möjligheter med try/except

Dokumentation

try:
    # Programkod som vi misstänker kan kasta undantag
except ZeroDivisionError:
    # Här hanterar vi division med noll
except (NameError, TypeError):
    # Här hanterar vi både NameError och TypeError
except:
    # Här hanterar vi alla undantag undantag
else:
    # Denna kod körs om inga undantag kastades
finally:
    # Denna kod körs alltid, oavsett om något gick fel eller inte,
    # och kan t.ex. användas för att städa upp efteråt.

Vad är poängen med finally? Kan man inte bara lägga koden direkt efter? En skillnad är att finally-satsen körs även om det inträffade ett fel som inte fångades upp, så att metoden skulle avbrytas:

try:
    file = open("foo.txt")
    contents = file.read()
finally:
    # Körs även om vi fick FileNotFoundError,
    # men sedan avbryts metoden

En annan skillnad är att finally-satsen körs även om vi uttryckligen avbryter koden i try, except eller else med hjälp av t.ex. "return".

def foo():
    try:
        return "x"
    finally:
        print("Hello")

>>> foo()
Hello
'x'

Här börjar vi alltså returnera "x". Innan vi faktiskt kan returnera körs finally-satsen, men sedan fortsätter man och returnerar "x". Detta kan se väldigt underligt ut, men håll i minnet att meningen inte är att man ska skriva ut saker i "finally" utan att man ska kunna kunna städa upp efter den kod man körde, genom att t.ex. alltid komma ihåg att radera en temporärfil man skapade.

Att kasta undantag

Vi kan när som helst kasta ett undantag som ett sätt att hoppa ur den aktuella programkörningen. Om det finns en undantagshanterare hamnar vi i den, annars avbryts körningen. Om vi vill kasta ett undantag (t.ex. ValueError) kan vi skriva raise ValueError.

Dokumentation


Sidansvarig: Peter Dalenius
Senast uppdaterad: 2021-12-03