8. Fel och undantag

Hittills har felmeddelanden inte mer än nämnts, men om du har provat exemplen har du förmodligen sett några. Det finns (minst) två typer av fel som kan särskiljas: syntaxfel och undantag.

8.1. Syntaxfel

Syntaxfel, även kända som parsingfel, är kanske den vanligaste typen av klagomål du får medan du fortfarande lär dig Python:

>>> while True print('Hello world')
  File "<stdin>", line 1
    while True print('Hello world')
               ^^^^^
SyntaxError: invalid syntax

Parsern upprepar den felaktiga raden och visar små pilar som pekar på den plats där felet upptäcktes. Observera att detta inte alltid är den plats som behöver åtgärdas. I exemplet upptäcks felet vid funktionen print(), eftersom det saknas ett kolon (':') precis före den.

Filnamnet (<stdin> i vårt exempel) och radnumret skrivs ut så att du vet var du ska leta om indata kom från en fil.

8.2. Undantag

Även om en sats eller ett uttryck är syntaktiskt korrekt kan det orsaka ett fel när man försöker exekvera det. Fel som upptäcks under exekveringen kallas undantag och är inte ovillkorligen fatala: du kommer snart att lära dig hur du hanterar dem i Python-program. De flesta undantag hanteras dock inte av program, utan resulterar i felmeddelanden som visas här:

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    10 * (1/0)
          ~^~
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    4 + spam*3
        ^^^^
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    '2' + 2
    ~~~~^~~
TypeError: can only concatenate str (not "int") to str

Den sista raden i felmeddelandet anger vad som hände. Undantag finns i olika typer och typen skrivs ut som en del av meddelandet: typerna i exemplet är ZeroDivisionError, NameError och TypeError. Den sträng som skrivs ut som undantagstyp är namnet på det inbyggda undantag som inträffade. Detta gäller för alla inbyggda undantag, men behöver inte gälla för användardefinierade undantag (även om det är en användbar konvention). Standardnamn på undantag är inbyggda identifierare (inte reserverade nyckelord).

Resten av raden innehåller detaljer som baseras på typen av undantag och vad som orsakade det.

Den föregående delen av felmeddelandet visar det sammanhang där undantaget inträffade, i form av en stack traceback. I allmänhet innehåller det en stack traceback som listar källrader, men det kommer inte att visa rader som läses från standardinmatningen.

Inbyggda undantag listar de inbyggda undantagen och deras innebörd.

8.3. Hantering av undantag

Det är möjligt att skriva program som hanterar utvalda undantag. Titta på följande exempel, som ber användaren om inmatning tills ett giltigt heltal har matats in, men tillåter användaren att avbryta programmet (med hjälp av Control-C eller vad operativsystemet stöder); notera att ett användargenererat avbrott signaleras genom att lyfta KeyboardInterrupt undantaget.

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...         break
...     except ValueError:
...         print("Oops!  That was no valid number.  Try again...")
...

Satsen try fungerar på följande sätt.

  • Först utförs försöksklausulen (satsen eller satserna mellan nyckelorden try och except).

  • Om inget undantag inträffar hoppas except-klausulen över och körningen av try-satsen avslutas.

  • Om ett undantag inträffar under exekveringen av try-satsen, hoppas resten av satsen över. Om dess typ matchar undantaget som anges efter nyckelordet except, exekveras except-satsen, och sedan fortsätter exekveringen efter try/except-blocket.

  • Om ett undantag inträffar som inte stämmer överens med undantaget som anges i except-satsen, skickas det vidare till yttre try-satser; om ingen hanterare hittas är det ett ohanterat undantag och exekveringen stoppas med ett felmeddelande.

En try-sats kan ha mer än en except-klausul för att ange hanterare för olika undantag. Högst en hanterare kommer att exekveras. Hanterarna hanterar bara undantag som förekommer i motsvarande try-sats, inte i andra hanterare av samma try-sats. En except-sats kan namnge flera undantag som en parentesförsedd tupel, till exempel:

... utom (RuntimeError, TypeError, NameError):
... pass

En klass i en except-sats matchar undantag som är instanser av klassen själv eller en av dess härledda klasser (men inte tvärtom — en except-sats som listar en härledd klass matchar inte instanser av dess basklasser). Till exempel kommer följande kod att skriva ut B, C, D i den ordningen:

klass B(Undantag):
    pass

klass C(B):
    pass

klass D(C):
    godkänd

för cls i [B, C, D]:
    try:
        raise cls()
    utom D:
        print("D")
    except C:
        print("C")
    utom B:
        print("B")

Observera att om except-klausulerna hade varit omvända (med except B först), skulle det ha skrivit ut B, B, B — den första matchande except-klausulen utlöses.

När ett undantag inträffar kan det ha associerade värden, även kallade undantagets argument. Vilka argument som finns och vilka typer av argument som finns beror på undantagstypen.

except-klausulen kan ange en variabel efter undantagsnamnet. Variabeln är bunden till undantagsinstansen som vanligtvis har ett args-attribut som lagrar argumenten. För enkelhetens skull definierar inbyggda undantagstyper __str__() för att skriva ut alla argument utan att uttryckligen komma åt .args.

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))    # the exception type
...     print(inst.args)     # arguments stored in .args
...     print(inst)          # __str__ allows args to be printed directly,
...                          # but may be overridden in exception subclasses
...     x, y = inst.args     # unpack args
...     print('x =', x)
...     print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

Undantagets __str__()-utdata skrivs ut som den sista delen (”detail”) av meddelandet för ohanterade undantag.

BaseException är den gemensamma basklassen för alla undantag. En av dess underklasser, Exception, är basklassen för alla icke-fatal undantag. Undantag som inte är underklasser till Exception hanteras normalt inte, eftersom de används för att ange att programmet ska avslutas. De inkluderar SystemExit som utlöses av sys.exit() och KeyboardInterrupt som utlöses när en användare vill avbryta programmet.

Exception kan användas som ett jokertecken som fångar upp (nästan) allt. Det är dock god praxis att vara så specifik som möjligt med de typer av undantag som vi avser att hantera, och att låta oväntade undantag sprida sig vidare.

Det vanligaste mönstret för att hantera Exception är att skriva ut eller logga undantaget och sedan höja det igen (så att en anropare också kan hantera undantaget):

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("OS error:", err)
except ValueError:
    print("Could not convert data to an integer.")
except Exception as err:
    print(f"Unexpected {err=}, {type(err)=}")
    raise

Satsen tryexcept har en valfri else-klausul som, när den finns, måste följa efter alla except-klausuler. Den är användbar för kod som måste köras om try-satsen inte ger upphov till ett undantag. Till exempel:

för arg i sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except OSError:
        print('kan inte öppna', arg)
    else:
        print(arg, 'har', len(f.readlines()), 'rader')
        f.close()

Användningen av else-satsen är bättre än att lägga till ytterligare kod till try-satsen eftersom den undviker att av misstag fånga ett undantag som inte uppstod av koden som skyddas av tryexcept-satsen.

Undantagshanterare hanterar inte bara undantag som inträffar direkt i försökssatsen, utan även de som inträffar inuti funktioner som anropas (även indirekt) i försökssatsen. Till exempel:

>>> def this_fails():
...     x = 1/0
...
>>> try:
...     this_fails()
... except ZeroDivisionError as err:
...     print('Handling run-time error:', err)
...
Handling run-time error: division by zero

8.4. Utredning av undantag

Med raise kan programmeraren tvinga ett specificerat undantag att inträffa. Till exempel:

>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    raise NameError('HiThere')
NameError: HiThere

Det enda argumentet till raise anger det undantag som skall tas upp. Detta måste vara antingen en undantagsinstans eller en undantagsklass (en klass som härstammar från BaseException, t.ex. Exception eller en av dess underklasser). Om en undantagsklass skickas, kommer den att instansieras implicit genom att anropa dess konstruktör utan argument:

raise ValueError # förkortning för "raise ValueError()

Om du behöver avgöra om ett undantag har uppstått, men inte tänker hantera det, kan du använda en enklare form av raise för att upprepa undantaget:

>>> try:
...     raise NameError('HiThere')
... except NameError:
...     print('An exception flew by!')
...     raise
...
An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    raise NameError('HiThere')
NameError: HiThere

8.5. Kedjning av undantag

Om ett ohanterat undantag inträffar i ett avsnitt med except, kommer det undantag som hanteras att kopplas till det och inkluderas i felmeddelandet:

>>> try:
...     open("database.sqlite")
... except OSError:
...     raise RuntimeError("unable to handle error")
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    open("database.sqlite")
    ~~~~^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
    raise RuntimeError("unable to handle error")
RuntimeError: unable to handle error

För att indikera att ett undantag är en direkt följd av ett annat, tillåter raise-satsen en valfri from-klausul:

# exc måste vara en undantagsinstans eller None.
raise RuntimeError från exc

Detta kan vara användbart när du transformerar undantag. Till exempel:

>>> def func():
...     raise ConnectionError
...
>>> try:
...     func()
... except ConnectionError as exc:
...     raise RuntimeError('Failed to open database') from exc
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    func()
    ~~~~^^
  File "<stdin>", line 2, in func
ConnectionError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
    raise RuntimeError('Failed to open database') from exc
RuntimeError: Failed to open database

Det gör det också möjligt att inaktivera automatisk undantagskedja med hjälp av idiomet from None:

>>> try:
...     open('database.sqlite')
... except OSError:
...     raise RuntimeError from None
...
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
    raise RuntimeError from None
RuntimeError

För mer information om kedjemekanik, se Inbyggda undantag.

8.6. Användardefinierade undantag

Program kan namnge sina egna undantag genom att skapa en ny undantagsklass (se Klasser för mer information om Python-klasser). Undantag bör typiskt härledas från klassen Exception, antingen direkt eller indirekt.

Undantagsklasser kan definieras som gör allt som andra klasser kan göra, men de är vanligtvis enkla och erbjuder ofta bara ett antal attribut som gör att information om felet kan extraheras av hanterare för undantaget.

De flesta undantag definieras med namn som slutar på ”Error”, vilket liknar namngivningen av standardundantagen.

Många standardmoduler definierar sina egna undantag för att rapportera fel som kan uppstå i funktioner som de definierar.

8.7. Definiera saneringsåtgärder

Satsen try har en annan valfri klausul som är avsedd att definiera rensningsåtgärder som måste utföras under alla omständigheter. Till exempel:

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print('Goodbye, world!')
...
Goodbye, world!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    raise KeyboardInterrupt
KeyboardInterrupt

Om en finally-klausul finns, kommer finally-klausulen att utföras som den sista uppgiften innan try-satsen avslutas. Klausulen finally körs oavsett om satsen try ger upphov till ett undantag eller inte. I följande punkter diskuteras mer komplexa fall när ett undantag inträffar:

  • Om ett undantag inträffar under exekveringen av try-satsen, kan undantaget hanteras av en except-sats. Om undantaget inte hanteras av en except-sats, kommer undantaget att tas upp igen efter att finally-satsen har exekverats.

  • Ett undantag kan inträffa under exekvering av en except eller else klausul. Återigen, undantaget tas upp igen efter att finally-satsen har exekverats.

  • Om finally-satsen exekverar en break-, continue- eller return-sats, aktiveras inte undantag på nytt. Detta kan vara förvirrande och avrådes därför. Från och med version 3.14 skickar kompilatorn ut en SyntaxWarning för detta (se PEP 765).

  • Om try-satsen når en break-, continue- eller return-sats, kommer finally-satsen att exekveras strax före break-, continue- eller return-satsens exekvering.

  • Om en finally-sats innehåller en return-sats, kommer det returnerade värdet att vara det från finally-satsens return-sats, inte värdet från try-satsens return-sats. Detta kan vara förvirrande och avrådes därför. Från version 3.14 ger kompilatorn ut en SyntaxWarning för detta (se PEP 765).

Till exempel:

>>> def bool_return():
...     try:
...         return True
...     finally:
...         return False
...
>>> bool_return()
False

Ett mer komplicerat exempel:

>>> def divide(x, y):
...     try:
...         result = x / y
...     except ZeroDivisionError:
...         print("division by zero!")
...     else:
...         print("result is", result)
...     finally:
...         print("executing finally clause")
...
>>> divide(2, 1)
result is 2.0
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    divide("2", "1")
    ~~~~~~^^^^^^^^^^
  File "<stdin>", line 3, in divide
    result = x / y
             ~~^~~
TypeError: unsupported operand type(s) for /: 'str' and 'str'

Som du kan se utförs finally-satsen i vilket fall som helst. Det TypeError som uppstår när två strängar delas upp hanteras inte av except-satsen och uppstår därför igen efter att finally-satsen har exekverats.

I verkliga tillämpningar är klausulen finally användbar för att frigöra externa resurser (t.ex. filer eller nätverksanslutningar), oavsett om användningen av resursen var framgångsrik eller inte.

8.8. Fördefinierade saneringsåtgärder

Vissa objekt definierar standardstädningsåtgärder som ska vidtas när objektet inte längre behövs, oavsett om operationen som använder objektet lyckades eller misslyckades. Titta på följande exempel, som försöker öppna en fil och skriva ut dess innehåll på skärmen.

för rad i open("minfil.txt"):
    print(rad, slut="")

Problemet med den här koden är att den lämnar filen öppen under en obestämd tid efter att den här delen av koden har exekverats. Detta är inte ett problem i enkla skript, men kan vara ett problem för större applikationer. Med with kan objekt som filer användas på ett sätt som säkerställer att de alltid rensas upp snabbt och korrekt.

med open("minfil.txt") som f:
    för rad i f:
        print(rad, slut="")

Efter att satsen har utförts stängs alltid filen f, även om det uppstod ett problem under bearbetningen av raderna. Objekt som, i likhet med filer, har fördefinierade rensningsåtgärder kommer att ange detta i sin dokumentation.

8.9. Upphävande och hantering av flera orelaterade undantag

Det finns situationer där det är nödvändigt att rapportera flera undantag som har inträffat. Detta är ofta fallet i ramverk för samtidighet, när flera uppgifter kan ha misslyckats parallellt, men det finns också andra användningsfall där det är önskvärt att fortsätta exekveringen och samla in flera fel i stället för att lösa ut det första undantaget.

Den inbyggda ExceptionGroup omsluter en lista med undantagsinstanser så att de kan tas upp tillsammans. Det är ett undantag i sig, så det kan fångas som vilket annat undantag som helst.

>>> def f():
...     excs = [OSError('error 1'), SystemError('error 2')]
...     raise ExceptionGroup('there were problems', excs)
...
>>> f()
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |     f()
  |     ~^^
  |   File "<stdin>", line 3, in f
  |     raise ExceptionGroup('there were problems', excs)
  | ExceptionGroup: there were problems (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | OSError: error 1
    +---------------- 2 ----------------
    | SystemError: error 2
    +------------------------------------
>>> try:
...     f()
... except Exception as e:
...     print(f'caught {type(e)}: e')
...
caught <class 'ExceptionGroup'>: e
>>>

Genom att använda except* istället för except kan vi selektivt hantera endast de undantag i gruppen som matchar en viss typ. I följande exempel, som visar en nästlad undantagsgrupp, extraherar varje except*-klausul undantag av en viss typ från gruppen medan alla andra undantag sprids till andra klausuler och så småningom återkallas:

>>> def f():
...     raise ExceptionGroup(
...         "group1",
...         [
...             OSError(1),
...             SystemError(2),
...             ExceptionGroup(
...                 "group2",
...                 [
...                     OSError(3),
...                     RecursionError(4)
...                 ]
...             )
...         ]
...     )
...
>>> try:
...     f()
... except* OSError as e:
...     print("There were OSErrors")
... except* SystemError as e:
...     print("There were SystemErrors")
...
There were OSErrors
There were SystemErrors
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 2, in <module>
  |     f()
  |     ~^^
  |   File "<stdin>", line 2, in f
  |     raise ExceptionGroup(
  |     ...<12 lines>...
  |     )
  | ExceptionGroup: group1 (1 sub-exception)
  +-+---------------- 1 ----------------
    | ExceptionGroup: group2 (1 sub-exception)
    +-+---------------- 1 ----------------
      | RecursionError: 4
      +------------------------------------
>>>

Observera att undantagen i en undantagsgrupp måste vara instanser, inte typer. Detta beror på att undantagen i praktiken vanligtvis är sådana som redan har tagits upp och fångats upp av programmet, enligt följande mönster:

>>> excs = []
... for test in tests:
...     try:
...         test.run()
...     except Exception as e:
...         excs.append(e)
...
>>> if excs:
...    raise ExceptionGroup("Test Failures", excs)
...

8.10. Berika undantag med anteckningar

När ett undantag skapas för att fångas upp initialiseras det vanligtvis med information som beskriver det fel som har inträffat. Det finns fall där det är användbart att lägga till information efter att undantaget har fångats. För detta ändamål har undantag en metod add_note(note) som accepterar en sträng och lägger till den i undantagets anteckningslista. Standardåtergivningen av spårningen innehåller alla anteckningar, i den ordning de lades till, efter undantaget.

>>> try:
...     raise TypeError('bad type')
... except Exception as e:
...     e.add_note('Add some information')
...     e.add_note('Add some more information')
...     raise
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
    raise TypeError('bad type')
TypeError: bad type
Add some information
Add some more information
>>>

När vi t.ex. samlar undantag i en undantagsgrupp kan vi vilja lägga till kontextinformation för de enskilda felen. I det följande har varje undantag i gruppen en anteckning som anger när detta fel har inträffat.

>>> def f():
...     raise OSError('operation failed')
...
>>> excs = []
>>> for i in range(3):
...     try:
...         f()
...     except Exception as e:
...         e.add_note(f'Happened in Iteration {i+1}')
...         excs.append(e)
...
>>> raise ExceptionGroup('We have some problems', excs)
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |     raise ExceptionGroup('We have some problems', excs)
  | ExceptionGroup: We have some problems (3 sub-exceptions)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 1
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 2
    +---------------- 3 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 3
    +------------------------------------
>>>