Lektion 3 · Cirka 45 min genomgång

Fel och undantag
Errors and exceptions

Ett robust program förutsätter inte att all inmatning är giltig. Det upptäcker förväntade problem, förklarar vad som gick fel och låter användaren försöka igen.

Om texten inte kan omvandlas till typen int skapas ett ValueError. En specifik except-gren kan hantera just det problemet.

alder.pyPython
try:
    alder = int(input("Ålder: "))
except ValueError:
    print("Skriv ett heltal.")
ResultatSkriv ett heltal.

Före genomgången

Se filmen och öva på att läsa felmeddelanden

Harvards lecture 3 heter Exceptions. Se filmen före lektionen. Ha en terminal och en editor öppna så att du kan framkalla små fel medvetet och sedan tolka dem.

2

Skapa en mapp

Skapa lektion_03 i kursmappen och öppna den i VS Code.

3

Arbeta systematiskt

Läs alltid sista raden i en traceback först. Den anger normalt undantagets typ och meddelande.

Tre frågor vid ett fel

Vilken typ av fel rapporteras? På vilken rad upptäcktes det? Vilket värde eller antagande gjorde att operationen inte kunde genomföras?

Dagens karta

Från felmeddelande till återhämtning

Vi skiljer först feltyperna åt och bygger sedan en kontrollerad väg tillbaka till normal körning.

operationnågot försöksexceptionnormal väg avbrytshandlerrätt gren väljsfortsättningprogrammet återhämtar sig
01

Läs signalen

Fel, undantag och traceback är olika delar

Ett syntaxfel betyder att Python inte kan tolka programtextens grammatik. Ett undantag, på engelska exception, uppstår när ett syntaktiskt giltigt program inte kan genomföra en operation under körning.

Före körning

SyntaxError

En saknad avslutande parentes, ett glömt kolon eller ett felaktigt indrag kan göra att Python inte kan läsa programmet. Källkoden måste rättas innan den delen kan köras.

Under körning

Exception

Syntaxen är giltig, men en viss operation misslyckas: text kan exempelvis inte bli int, en nämnare är noll eller en nyckel saknas i ett dict.

Läs en traceback nedifrån och upp

En traceback visar den kedja av anrop som ledde till ett ohanterat undantag. Sista raden innehåller normalt undantagets klass, exempelvis ValueError, och ett meddelande. Raderna ovan visar filer, radnummer och funktionsanrop.

Pilen markerar var Python upptäckte problemet. Den bakomliggande orsaken kan ligga tidigare, till exempel i ett oväntat värde som skickades in till funktionen.

Typ
Vilken sorts problem uppstod?
Meddelande
Vilken detalj kan Python ge om orsaken?
Plats
I vilken fil och på vilken rad upptäcktes felet?
Anropskedja
Vilka funktionsanrop ledde fram till raden?
traceback.txtPython
Traceback (most recent call last):
  File "alder.py", line 1, in <module>
    alder = int("trettio")
ValueError: invalid literal for int()
ValueErrorrätt typ av argument, men ogiltigt värde
TypeErroroperationen stöder inte den givna typen
NameErrornamnet är inte definierat
ZeroDivisionErrordivision eller modulo med noll
IndexErrorindex finns inte i sekvensen
KeyErrornyckeln finns inte i ett dict
EOFErrorinput når slut på indata
SyntaxError fångas normalt inte runt den felaktiga raden

Python måste först kunna tolka filen innan satserna börjar köras. En vanlig try-sats i samma felaktiga fil kan därför inte rädda syntax som inte går att läsa.

02

Hantera förväntade problem

try och except delar kontrollflödet

I ett try-block placeras den minsta operation som förväntas kunna skapa ett visst undantag. Om undantaget uppstår avbryts resten av blocket och Python söker uppifrån efter den första matchande except-grenen.

heltal.pyPython
try:
    tal = int(input("Heltal: "))
except ValueError:
    print("Du skrev inte ett heltal.")
ResultatDu skrev inte ett heltal.

Exakt körordning

  1. Python börjar köra det indragna try-blocket.
  2. Om omvandlingen lyckas hoppas except ValueError över.
  3. Om int skapar ValueError avbryts återstoden av try.
  4. Den matchande handlern körs och programmet fortsätter därefter.

En handler är koden i en except-gren. Den ska bara fånga ett problem som programmet faktiskt kan hantera på ett meningsfullt sätt.

Olika åtgärder

Flera except-grenar

dividera.pyPython
try:
    svar = taljare / namnare
except TypeError:
    print("Använd tal.")
except ZeroDivisionError:
    print("Nämnaren får inte vara noll.")
Samma åtgärd

En tupel med typer

dividera.pyPython
try:
    svar = taljare / namnare
except (TypeError, ZeroDivisionError):
    print("Divisionen kunde inte göras.")

Undantagsobjektet kan få ett namn

Med as error binds det fångade undantagsobjektet till variabeln error. Objektet innehåller bland annat meddelandet från Python. Det är användbart för loggning och felsökning, men råa tekniska meddelanden är inte alltid lämpliga som användarinstruktion.

meddelande.pyPython
try:
    tal = int(text)
except ValueError as error:
    print(f"Teknisk detalj: {error}")
Undvik en tom except:

En gren utan angiven typ fångar nästan allt och kan dölja programmeringsfel eller avbrott. Även except Exception är ofta för brett i nybörjarkod. Fånga i stället de specifika, förväntade undantag som du kan hantera.

03

Den lyckade vägen och den garanterade vägen

else körs vid framgång · finally körs alltid

else hör här till try-satsen och körs bara om inget undantag uppstod i try-blocket. finally körs när kontrollen lämnar satsen, oavsett om ett undantag inträffade.

Tilldelning sker först efter lyckad beräkning

I tal = int(text) utvärderas högerledet int(text) först. Om det skapar ValueError sker ingen tilldelning och tal får inget nytt värde. Att använda namnet efteråt kan därför ge NameError.

Lägg den lyckade fortsättningen i else. Då syns det att den bara får använda resultatet när try lyckades.

else.pyPython
try:
    tal = int(text)
except ValueError:
    print("Ogiltigt heltal.")
else:
    print(f"Dubbelt: {tal * 2}")
Bra avgränsning

Håll try litet

Ju färre rader som ligger i blocket, desto tydligare är det vilken operation som förväntas misslyckas. Kod i else fångas inte av de föregående handler-grenarna av misstag.

Städning

finally

Används bland annat för att frigöra resurser eller skriva en avslutande loggrad. För filer är with ofta ett ännu tydligare sätt att säkerställa stängning.

finally.pyPython
try:
    resultat = 10 / namnare
except ZeroDivisionError:
    print("Division med noll går inte.")
else:
    print(resultat)
finally:
    print("Försöket är avslutat.")
ResultatFörsöket är avslutat.
04

Återhämtning

En loop kan låta användaren försöka igen

Att fånga ett undantag gör inte automatiskt inmatningen giltig. En while True-loop kan upprepa frågan tills omvandlingen lyckas och funktionen lämnar ett godkänt värde.

get_int.pyPython
def get_int(prompt):
    while True:
        try:
            return int(input(prompt))
        except ValueError:
            print("Skriv ett heltal.")

alder = get_int("Ålder: ")

return avslutar både loop och funktion

Vid giltig inmatning returneras heltalet direkt. Då avslutas hela funktionsanropet, inte bara loopen. Vid ValueError körs handlern och nästa iteration börjar.

break
Avslutar den närmaste loopen; funktionen fortsätter efter loopen.
return
Avslutar hela funktionsanropet och kan lämna ett värde.
pass
Gör ingenting; körningen fortsätter med nästa sats.
pass är en tom sats, inte en felhantering i sig

I en except-gren kan pass användas för att tyst försöka igen, men återkoppling är ofta mer pedagogisk. Använd det bara när tystnad är ett medvetet krav, exempelvis när en okänd menyartikel ska ignoreras.

EOFError signalerar slut på inmatningen

input skapar EOFError om indata tar slut. Det används i uppgifter där ett okänt antal rader läses. I en lokal terminal skickas EOF normalt med Ctrl+D på macOS och Linux, och ofta med Ctrl+Z följt av Enter på Windows.

Handlern kan då avsluta loopen med break. Ett vanligt mönster är att samla data i ett dict under loopen och skriva ett sorterat resultat efteråt.

rader.pyPython
rader = []

while True:
    try:
        rader.append(input("Rad: "))
    except EOFError:
        break

print(rader)
05

Skapa tydliga fel själv

raise rapporterar ett brutet kontrakt

Ett värde kan ha rätt typ men ändå bryta mot programmets regler. Med raise skapar programmet avsiktligt ett undantag som beskriver problemet.

procent.pyPython
def procent(andel):
    if not 0 <= andel <= 1:
        raise ValueError("andel måste ligga mellan 0 och 1")
    return andel * 100

Validera nära regeln

Funktionen har ett kontrakt: argumentet ska ligga i intervallet 0–1. raise ValueError(...) avbryter funktionen om kontraktet bryts. Anroparen kan sedan välja att fånga undantaget eller låta det fortsätta uppåt.

Använd en inbyggd typ när den beskriver problemet väl. Det gör koden lättare att förstå för andra Pythonprogrammerare.

Förväntat fel

raise ValueError

Passar för validering av data och kan hanteras även när Python körs optimerat.

Utvecklarantagande

assert villkor

Kontrollerar ett internt antagande under utveckling. assert kan tas bort i optimerat körläge och ska därför inte användas för normal användarinmatning.

assertion.pyPython
def medelvarde(tal):
    assert tal, "intern regel: listan får inte vara tom"
    return sum(tal) / len(tal)
Använd inte assert som inmatningsfilter

Användaren förväntas ibland skriva fel. Det är en normal situation som ska valideras uttryckligen och hanteras med exempelvis ValueError, inte ett utvecklarantagande som kan kopplas bort.

Egna undantagsklasser är en fördjupning

En egen klass kan uttrycka ett domänspecifikt problem. Den ärver normalt från Exception och namnet slutar konventionellt med Error. Eftersom klasser kommer senare i kursen räcker det nu att känna igen formen. Välj en inbyggd typ om den redan beskriver problemet.

egna_fel.pyPython
class NotEvenError(Exception):
    pass

if tal % 2 != 0:
    raise NotEvenError("talet måste vara jämnt")
06

Defensiv programmering

Normalisera, omvandla, validera och bearbeta

Robust kod delar upp arbetet i begripliga steg. Det gör att varje undantag kan kopplas till en viss operation och att regler kan testas utan interaktiv inmatning.

normaliserastrip, loweromvandlaint, floatvalideradomänens reglerbearbetaberäkna eller lagra

Fånga där du kan återhämta dig

En låg nivå kan upptäcka och rapportera att ett värde är ogiltigt med raise. Den interaktiva delen kan fånga undantaget, ge ett begripligt meddelande och fråga igen. Om en funktion inte vet hur problemet ska lösas bör den normalt inte dölja det.

Skilj på datafel och programmeringsfel. En användare som skriver tre där ett heltal krävs är förväntat. Ett felstavat variabelnamn som ger NameError är ett kodfel som ska rättas, inte tyst fångas.

las_andel.pyPython
def parse_fraction(text):
    delar = text.split("/")
    if len(delar) != 2:
        raise ValueError("formatet ska vara X/Y")

    x, y = map(int, delar)
    if x > y:
        raise ValueError("X får inte vara större än Y")
    return x / y
Funktionen förklaras rad för rad

split("/") delar strängen vid tecknet /. map(int, delar) tillämpar int på varje del, och uppackningen binder de två resultaten till x och y. ValueError beskriver fel format eller ogiltig relation; division med y == 0 ger ZeroDivisionError.

Under utveckling

Testa varje felväg

Skriv testfall för giltig inmatning, tom text, fel format, bokstäver, gränsvärden och noll. Ett undantag är en del av funktionens beteende och ska testas avsiktligt.

Till användaren

Ge handlingsbar återkoppling

Säg vad som krävs: ”Skriv två heltal som X/Y” är bättre än ”Något gick fel”. Behåll tekniska detaljer i utvecklarens traceback eller logg.

Problem Set 3

Fyra övningar i robust inmatning

Harvards aktuella uppgiftssidor är kravspecifikationen. Äldre lokala exempel kan avvika och ska inte behandlas som facit.

01

Fuel Gauge

Läs bråket X/Y, skriv närmaste heltalsprocent och använd E vid högst 1 procent samt F vid minst 99 procent. Fråga igen vid icke-heltal, X > Y eller Y == 0.

Testa: 3/4, 1/100, 99/100, 4/4, 5/4, 3/0 och cat/dog.

Aktuell uppgift ↗
02

Felipe's Taqueria

Läs artiklar till EOFError, gör uppslaget skiftlägesoberoende och skriv löpande totalsumma med dollartecken och två decimaler efter varje giltig artikel. Okända artiklar ignoreras.

Testa: olika skiftläge, flera giltiga köp, okänd artikel och avslut med EOF.

Aktuell uppgift ↗
03

Grocery List

Räkna förekomster i ett dict tills input skapar EOFError. Skriv sedan antalet följt av varan i versaler, sorterat alfabetiskt efter vara.

Testa: samma vara med olika skiftläge, en enda vara, flera varor i osorterad ordning och EOF.

Aktuell uppgift ↗
04

Outdated

Acceptera datum som 9/8/1636 eller September 8, 1636 och skriv 1636-09-08. Ogiltigt format, månad utanför 1–12 eller dag utanför 1–31 ska ge ett nytt försök.

Testa: båda formaten, inledande nollor, okänd månad, dag 32 och ofullständiga datum.

Aktuell uppgift ↗
Kontrollera alltid den aktuella specifikationen

Exempelvis kostar Baja Taco nu 4,25 dollar, 4/4 är giltigt och ska ge F, och ett ISO-datum använder vanliga bindestreck samt två siffror för månad och dag.

Fortsätt arbeta

Resurser och självkontroll

Använd notebooken för experiment, guiden för begreppen och originalkällorna för den aktuella kravspecifikationen.

Efter lektionen kan jag …

skilja SyntaxError från undantag under körning

läsa typ, meddelande och plats i en traceback

fånga specifika undantag med try och except

förklara else, finally, raise och pass

bygga en loop som frågar igen efter ogiltig inmatning

förklara varför assert inte ersätter validering