Professionellt felhantering med Python

I den här handledningen lär du dig hur du hanterar felförhållandena i Python från en hel systemsynvinkel. Felhantering är en kritisk aspekt av designen, och den går från de lägsta nivåerna (ibland hårdvaran) hela vägen till slutanvändarna. Om du inte har en konsekvent strategi på plats, kommer ditt system att vara opålitligt, användarupplevelsen blir dålig, och du kommer ha många utmaningar debugging och felsökning. 

Nyckeln till framgång är att vara medveten om alla dessa sammankopplande aspekter, med tanke på dem uttryckligen, och bilda en lösning som behandlar varje punkt.

Statuskoder vs undantag

Det finns två huvudsakliga felhanteringsmodeller: statuskoder och undantag. Statuskoder kan användas av något programmeringsspråk. Undantag kräver språk / runtime support. 

Python stöder undantag. Python och dess standardbibliotek använder undantag för att rapportera om många exceptionella situationer som IO-fel, dividera med noll, indexering utan gränser, och även några inte så exceptionella situationer som slutet av iteration (även om det är dolt). De flesta bibliotek följer efter och ger upphov till undantag.

Det betyder att din kod måste hantera de undantag som Python och biblioteken väckte i alla fall, så att du också kan hämta undantag från din kod vid behov och inte lita på statuskoder.

Snabbt exempel

Innan vi dyker in i Pythons undantag och felhantering av bästa praxis, låt oss se några undantagshantering i åtgärd:

def f (): returnera 4/0 def g (): höj undantag ("Ring inte till oss. Vi ringer dig") def h (): försök: f () utom undantag som e: print försök: g () förutom undantag som e: print (e)

Här är utgången när du ringer h ():

h () division med noll Ring inte till oss. Vi ringer dig

Python undantag

Python-undantag är objekt som är organiserade i en klasshierarki. 

Här är hela hierarkin:

BaseException + - SystemExit + - KeyboardInterrupt + - GeneratorExit + - Undantag + - StopIteration + - StandardError | + - BufferError | + - AritmeticError | | + - FloatingPointError | | + - OverflowError | | + - ZeroDivisionError | + - AssertionError | + - AttributeError | + - EnvironmentError | | + - IOError | | + - OSError | | + - WindowsError (Windows) | | + - VMSError (VMS) | + - EOFError | + - ImportError | + - LookupError | | + - IndexError | | + - KeyError | + - MemoryError | + - NameError | | + - ObundetLocalError | + - ReferenceError | + - RuntimeError | | + - NotImplementedError | + - SyntaxError | | + - IndentationError | | + - TabError | + - SystemError | + - TypeError | + - ValueError | + - UnicodeError | + - UnicodeDecodeError | + - UnicodeEncodeError | + - UnicodeTranslateError + - Varning + - DeprecationWarning + - PendingDeprecationWarning + - RuntimeWarning + - SyntaxWarning + - UserWarning + - FutureWarning + - ImportWarning + - UnicodeWarning + - BytesWarning  

Det finns flera speciella undantag som härrör direkt från BaseException, tycka om SystemExit, KeyboardInterrupt och GeneratorExit. Då finns det Undantag klass, vilken är basklassen för StopIteration, Standard fel och Varning. Alla standardfel härrör från Standard fel.

När du tar upp ett undantag eller någon funktion som du heter kallar ett undantag, upphör det normala kodflödet och undantaget börjar sprida upp samtalstacken tills det möter en riktig undantagshanterare. Om inget undantagshanterare är tillgängligt för att hantera det, kommer processen (eller mer exakt den nuvarande tråden) att avslutas med ett obehandlat undantagsmeddelande.

Öka undantag

Att höja undantag är väldigt lätt. Du använder bara höja sökord för att höja ett objekt som är en underklass av Undantag klass. Det kan vara en förekomst av Undantag i sig, ett av de vanliga undantagen (t.ex.. RuntimeError) eller en underklass av Undantag du härledde dig själv. Här är ett litet fragment som visar alla fall:

# Höj en förekomst av Undantagsklassen själv höja Undantag ("Ummm ... något är fel") # Höj en förekomst av RuntimeError-klassen höja RuntimeError ('Ummm ... något är fel') # Höj en egen underklass av Undantag som håller tidsstämpeln undantaget skapades från datetime import datetime class SuperError (Undantag): def __init __ (självmeddelande): Undantag .__ init __ (meddelande) self.when = datetime.now () höja SuperError ('Ummm ... någonting är fel')

Fångande undantag

Du fångar undantag med bortsett från klausul, som du såg i exemplet. När du får ett undantag har du tre alternativ:

  • Svälja det tyst (hantera det och fortsätt springa).
  • Gör något som att logga, men höja samma undantag för att låta högre nivåer hantera.
  • Höj ett annat undantag istället för originalet.

Svälja undantagen

Du bör svälja undantaget om du vet hur man hanterar det och kan fullt ut återhämta sig. 

Om du till exempel får en inmatningsfil som kan vara i olika format (JSON, YAML) kan du prova att analysera det med olika parsers. Om JSON-parsern höjde ett undantag om att filen inte är en giltig JSON-fil, sväljer du den och försöker med YAML-parsern. Om YAML-parsern misslyckades, så lät du undantaget sprida ut.

importera json import yaml def parse_file (filnamn): försök: returnera json.load (öppet (filnamn)) utom json.JSONDecodeError returnera yaml.load (öppet (filnamn))

Observera att andra undantag (till exempel fil som inte hittats eller inga läsbehörigheter) kommer att sprida ut och kommer inte att fångas av den specifika undantagsbestämmelsen. Det här är en bra policy i det här fallet där du bara vill pröva YAML-analyseringen om JSON-analysering misslyckades på grund av ett JSON-kodningsproblem. 

Om du vill hantera Allt undantag sedan bara använda utom undantag. Till exempel:

def print_exception_type (func, * args, ** kwargs): försök: returnera func (* args, ** kwargs) utom undantag som e: utskriftstyp (e)

Observera att genom att lägga till som e, du binder undantagsobjektet till namnet e tillgänglig i din undantagsklausul.

Återupprepa samma undantag

För att höjas, lägg bara till höja utan några argument i din hanterare. Detta gör att du kan utföra lite lokal hantering, men tillåter fortfarande att övre nivåer hanterar det också. Här, den invoke_function () funktionen skriver ut typen av undantag till konsolen och höjer sedan undantaget.

def invoke_function (func, * args, ** kwargs): försök: returnera func (* args, ** kwargs) utom undantag som e: utskriftstyp (e) höja

Höj en annan undantag

Det finns flera fall där du vill höja ett annat undantag. Ibland vill du gruppera flera olika lågnivå undantag till en enda kategori som hanteras enhetligt med högre nivå kod. I fallfall måste du ändra undantaget till användarnivån och ge ett visst användningsspecifikt sammanhang. 

Slutligen Clause

Ibland vill du se till att vissa rengöringskoder utförs även om ett undantag höjdes någonstans under vägen. Du kan till exempel ha en databasanslutning som du vill stänga när du är klar. Här är fel sätt att göra det:

def fetch_some_data (): db = open_db_connection () fråga (db) close_db_Connection (db)

Om fråga() funktionen ger upphov till ett undantag då samtalet till close_db_connection () kommer aldrig att köras och DB-anslutningen är öppen. De till sist klausulen körs alltid efter ett försök, utförs alla undantagshanterare. Så här gör du det korrekt:

def fetch_some_data (): db = Ingen försök: db = open_db_connection () fråga (db) äntligen: om db inte är None: close_db_connection (db)

Samtalet till open_db_connection () får inte returnera en anslutning eller göra ett undantag själv. I det här fallet behöver du inte stänga DB-anslutningen.

När man använder till sist, du måste vara försiktig med att inte göra några undantag där eftersom de kommer att maskera det ursprungliga undantaget.

Context Managers

Kontexthanterare ger en annan mekanism för att paketera resurser som filer eller DB-anslutningar i uppringningskoden som kör automatiskt även när undantag har höjts. Istället för att försöka slutligen blockera, använder du med påstående. Här är ett exempel med en fil:

def process_file (filnamn): med öppet (filnamn) som f: process (f.read ()) 

Nu, även om bearbeta() upphävde ett undantag, filen kommer att stängas ordentligt omedelbart när omfattningen av med blocket är avslutat, oavsett om undantaget hanterades eller inte.

Skogsavverkning

Logging är ganska mycket ett krav i icke-triviala, långlöpande system. Det är särskilt användbart i webbapplikationer där du kan behandla alla undantag på ett generiskt sätt: Logga bara på undantaget och returnera ett felmeddelande till den som ringer. 

När du loggar är det användbart att logga in undantagstypen, felmeddelandet och stacktrace. All denna information är tillgänglig via sys.exc_info objekt, men om du använder logger.exception () Metoden i din undantagshanterare, kommer Python-loggningssystemet att extrahera all relevant information till dig.

Detta är den bästa praxis jag rekommenderar:

importera loggar logger = logging.getLogger () def f (): försök: flaky_func () utom undantag: logger.exception () höja

Om du följer det här mönstret (då du förutsätter att du loggar in korrekt), oavsett vad som händer, får du en ganska bra post i dina loggar om vad som gick fel och du kommer att kunna lösa problemet.

Om du höjer igen, se till att du inte loggar samma undantag om och om igen på olika nivåer. Det är slöseri, och det kan förvirra dig och få dig att tro att flera fall av samma problem uppstod, när en enda instans i praktiken loggades flera gånger.

Det enklaste sättet att göra det är att låta alla undantag sprida sig (om inte de kan hanteras säkert och sväljas tidigare) och sedan göra loggen nära din applikations / systemets översta nivå.

Vakt

Logging är en förmåga. Det vanligaste genomförandet använder loggfiler. Men för stora distribuerade system med hundratals, tusentals eller fler servrar är det inte alltid den bästa lösningen. 

För att hålla reda på undantag över hela din infrastruktur är en tjänst som sentry super hjälpsam. Det centraliserar alla undantagsrapporter, och i tillägg till stacktrace lägger det till tillståndet för varje stapelram (värdet av variabler vid det tillfället som undantaget höjdes). Det ger också ett riktigt fint gränssnitt med instrumentpaneler, rapporter och sätt att bryta ner meddelandena genom flera projekt. Det är öppen källkod, så du kan köra din egen server eller prenumerera på den värdversionen.

Hantera övergående misslyckande

Några misslyckanden är tillfälliga, i synnerhet vid hantering av distribuerade system. Ett system som freaks ut vid första tecken på problem är inte särskilt användbart. 

Om din kod har åtkomst till något fjärrsystem som inte svarar, är den traditionella lösningen timeout, men ibland är inte alla system utformade med timeouts. Timeouts är inte alltid lätta att kalibrera när förhållandena ändras. 

Ett annat tillvägagångssätt är att misslyckas snabbt och försök igen. Fördelen är att om målet svarar snabbt, behöver du inte spendera mycket tid i viloläge och kan reagera omedelbart. Men om det misslyckades kan du försöka igen flera gånger tills du bestämmer att det verkligen är oåtkomligt och ger ett undantag. I nästa avsnitt presenterar jag en dekoratör som kan göra det för dig.

Användbara dekoratörer

Två dekoratörer som kan hjälpa till med felhantering är @log_error, som loggar ett undantag och sedan höjer det igen, och @Försök igen dekoratör, som försöker ringa en funktion flera gånger.

Felloggare

Här är en enkel implementering. Dekoratören tar bort ett loggerobjekt. När det dekorerar en funktion och funktionen påkallas, kommer den att sätta in samtalet i en försöksklausul, och om det fanns ett undantag kommer det att logga in och slutligen höja undantaget.

def log_error (logger) def dekorerade (f): @ functools.wraps (f) def wrapped (* args, ** kwargs): försök: returnera f (* args, ** kwargs) utom undantag som e: om logger: logger .exception (e) höja returförpackad returdekoration

Så här använder du den:

importera loggning logger = logging.getLogger () @log_error (logger) def f (): höja undantag ("jag är exceptionell")

Retrier

Här är en mycket bra implementering av @retry decoratorn.

importtid import matte # Försök igen dekoratör med exponentiell backoff def försök (försöker, fördröjning = 3, backoff = 2): "Retries en funktion eller metod tills den återgår True. fördröjning ställer in den ursprungliga fördröjningen i sekunder och backoff ställer in den faktor som förseningen bör förlängas efter varje misslyckande. backoff måste vara större än 1, annars är det inte riktigt en backoff. försök måste vara minst 0 och fördröja större än 0. "om backoff <= 1: raise ValueError("backoff must be greater than 1") tries = math.floor(tries) if tries < 0: raise ValueError("tries must be 0 or greater") if delay <= 0: raise ValueError("delay must be greater than 0") def deco_retry(f): def f_retry(*args, **kwargs): mtries, mdelay = tries, delay # make mutable rv = f(*args, **kwargs) # first attempt while mtries > 0: om rv är sant: # Klar på framgångsändring Sann mtries - = 1 # konsumera ett försökstid.sleep (mdelay) # vänta ... mdelay * = backoff # gör framtiden vänta längre rv = f (* args, ** kwargs) # Försök igen returnera False # Ran ur försök :-( returnera f_retry # true decorator -> dekorerad funktion returnera deco_retry # @retry (arg [, ...]) -> true decorator

Slutsats

Felhantering är avgörande för både användare och utvecklare. Python ger bra stöd i språk- och standardbiblioteket för undantagsbaserad felhantering. Genom att följa bästa praxis flitigt kan du erövra denna ofta försummade aspekten.

Lär Python

Lär dig Python med vår kompletta handledning för pythonhandledning, oavsett om du bara har börjat eller du är en erfaren kodare som vill lära dig nya färdigheter.