Profesjonell Feilhåndtering Med Python

I denne opplæringen lærer du hvordan du håndterer feilforholdene i Python fra et helhetssynspunkt. Feilhåndtering er et kritisk aspekt av design, og det krysser fra de laveste nivåene (noen ganger maskinvaren) helt til sluttbrukerne. Hvis du ikke har en konsekvent strategi på plass, vil systemet være upålitelig, brukeropplevelsen vil være dårlig, og du vil ha mange utfordringer feilsøking og feilsøking. 

Nøkkelen til suksess er å være oppmerksom på alle disse sammenlåsende aspektene, overveie dem eksplisitt, og danne en løsning som adresserer hvert punkt.

Statuskoder vs unntak

Det finnes to hovedfeilhåndteringsmodeller: statuskoder og unntak. Statuskoder kan brukes av hvilket som helst programmeringsspråk. Unntak krever språk / kjøretidstøtte. 

Python støtter unntak. Python og dets standardbibliotek bruker unntak å rapportere om mange eksepsjonelle situasjoner som IO-feil, divisjon med null, oversatt indeksering, og også noen ikke så eksepsjonelle situasjoner som slutten av iterasjon (selv om den er skjult). De fleste biblioteker følger etter og gir unntak.

Det betyr at koden din må håndtere unntakene som Python og biblioteker hevder uansett, så du kan også få unntak fra koden din når det er nødvendig og ikke stole på statuskoder.

Hurtigeksempel

Før du drar inn i det indre hukommelsen av Python unntak og feilhåndtering av beste praksis, la oss se noen unntakshåndtering i handling:

def f (): return 4/0 def g (): raise Exception ("Ikke ring oss. Vi ringer deg") def h (): prøv: f () unntatt Unntak som e: print prøv: g () unntatt unntak som e: print (e)

Her er resultatet når du ringer h ():

h () divisjon med null Ikke ring oss. Vi ringer deg

Python unntak

Python unntak er objekter organisert i et klasses hierarki. 

Her er hele hierarkiet:

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

Det er flere spesielle unntak som er avledet direkte fra BaseException, som SystemExit, KeyboardInterrupt og GeneratorExit. Så er det Unntak klasse, som er grunnklassen for StopIteration, Standard feil og Advarsel. Alle standardfeilene er avledet fra Standard feil.

Når du oppfordrer et unntak eller en funksjon som du kaller, gir et unntak, slutter den normale kodeflyten, og unntaket begynner å formere opp samtalestakken til det møter en riktig unntakshåndterer. Hvis ingen unntakshåndterer er tilgjengelig for å håndtere det, vil prosessen (eller mer nøyaktig gjeldende tråd) bli avsluttet med en ubehandlet unntaksmelding.

Øke unntak

Å heve unntak er veldig enkelt. Du bruker bare heve søkeord for å heve et objekt som er en underklasse av Unntak klasse. Det kan være en forekomst av Unntak i seg selv, en av standard unntakene (f.eks. RuntimeError), eller en underklasse av Unntak du har avledet deg selv. Her er en liten utgave som viser alle tilfeller:

# Løft en forekomst av Unntaksklassen selv, opprettholder Unntak ('Ummm ... noe er galt') # Løft en forekomst av RuntimeError-klassen, løft RuntimeError ('Ummm ... noe er galt') # Løft en tilpasset underklasse av Unntak som holder tidsstempelet unntaket ble opprettet fra datetime import datetime klasse SuperError (Unntak): def __init __ (selvmelding): Unntak .__ init __ (melding) self.when = datetime.now () heve SuperError ('Ummm ... noe er galt')

Fangende unntak

Du fanger unntak med unntatt klausul, som du så i eksemplet. Når du får et unntak, har du tre alternativer:

  • Svelg det stille (håndter det og fortsett å løpe).
  • Gjør noe som å logge, men re-raise det samme unntaket for å la høyere nivåer håndtere.
  • Løft et annet unntak i stedet for originalen.

Svelg unntaket

Du bør svelge unntaket hvis du vet hvordan du skal håndtere det og kan fullt ut gjenopprette. 

Hvis du for eksempel mottar en inngangsfil som kan være i forskjellige formater (JSON, YAML), kan du prøve å analysere det ved hjelp av forskjellige parsere. Hvis JSON-parseren hevet et unntak om at filen ikke er en gyldig JSON-fil, svelger du den og prøver med YAML-parseren. Hvis YAML-parseren mislyktes, så lar du unntaket spre seg ut.

import json import yaml def parse_file (filnavn): prøv: return json.load (åpen (filnavn)) unntatt json.JSONDecodeError return yaml.load (åpent (filnavn))

Vær oppmerksom på at andre unntak (for eksempel fil ikke funnet eller ingen lesetillatelser) vil formere seg og ikke bli fanget av den spesifikke unntaksklausulen. Dette er en god policy i dette tilfellet hvor du vil prøve YAML-analysering bare hvis JSON-analysering feilet på grunn av et JSON-kodingsproblem. 

Hvis du vil håndtere alle unntak så bare bruk unntatt unntak. For eksempel:

def print_exception_type (func, * args, ** kwargs): Prøv: return func (* args, ** kwargs) unntatt Unntak som e: utskriftstype (e)

Legg merke til at ved å legge til som e, Du binder unntaksobjektet til navnet e tilgjengelig i din unntatt klausul.

Gjenta den samme unntaket

For å øke, legg bare til heve uten argumenter inne i handleren din. Dette gjør at du kan utføre litt lokalhåndtering, men lar fortsatt øvre nivåer håndtere det også. Her, den invoke_function () funksjonen skriver ut typen unntak til konsollen og re-reiser unntaket.

def invoke_function (func, * args, ** kwargs): prøv: return func (* args, ** kwargs) unntatt unntak som e: utskriftstype (e) heve

Gjør en annen unntak

Det er flere tilfeller der du vil oppheve et annet unntak. Noen ganger vil du gruppere flere forskjellige lavnivå unntak i en enkelt kategori som håndteres jevnt etter høyere nivå kode. I rekkefølge må du forandre unntaket til brukernivå og gi noen applikasjonsspesifikke kontekster. 

Endelig Klausul

Noen ganger vil du sørge for at noen oppryddingskoder utføres selv om et unntak er reist et sted underveis. For eksempel kan du ha en databaseforbindelse som du vil lukke når du er ferdig. Her er feil måte å gjøre det på:

def fetch_some_data (): db = open_db_connection () spørring (db) close_db_Connection (db)

Hvis spørsmål() funksjonen gir et unntak enn anropet til close_db_connection () vil aldri utføre og DB-tilkoblingen forblir åpen. De endelig klausulen utføres alltid etter et forsøk, er all unntakshandler utført. Slik gjør du det riktig:

def fetch_some_data (): db = Ingen forsøk: db = open_db_connection () spørring (db) endelig: hvis db ikke er None: close_db_connection (db)

Anropet til open_db_connection () Kan ikke returnere en tilkobling eller opprette et unntak selv. I dette tilfellet er det ikke nødvendig å lukke DB-tilkoblingen.

Når du bruker endelig, du må være forsiktig så du ikke hevder noen unntak der, fordi de vil maskere det opprinnelige unntaket.

Kontekstledere

Kontekstledere gir en annen mekanisme for å pakke inn ressurser som filer eller DB-tilkoblinger i oppryddingskoden som kjøres automatisk, selv om unntak er oppdratt. I stedet for å prøve-endelig blokker, bruker du med uttalelse. Her er et eksempel med en fil:

def process_file (filnavn): med åpent (filnavn) som f: prosess (f.read ()) 

Nå, selv om prosess() hevet et unntak, vil filen bli stengt riktig umiddelbart når omfanget av med Blokken er avsluttet, uavhengig av om unntaket ble håndtert eller ikke.

logging

Logging er stort sett et krav i ikke-trivielle, langvarige systemer. Det er spesielt nyttig i webapplikasjoner hvor du kan behandle alle unntak på en generisk måte: Bare logg unntaket og send en feilmelding til den som ringer. 

Når du logger, er det nyttig å logge unntakstypen, feilmeldingen og stacktrace. All denne informasjonen er tilgjengelig via sys.exc_info objekt, men hvis du bruker logger.exception () metode i din unntakshåndterer, vil Python-loggingssystemet trekke ut all relevant informasjon for deg.

Dette er den beste praksisen jeg anbefaler:

importer logg logger = logging.getLogger () def f (): prøv: flaky_func () unntatt Unntak: logger.exception () heve

Hvis du følger dette mønsteret, så (hvis du forutsetter at du logger på riktig måte), uansett hva som skjer, får du en ganske god post i loggene dine på hva som gikk galt, og du vil kunne løse problemet.

Hvis du re-raise, må du forsikre deg om at du ikke logger det samme unntaket igjen og igjen på forskjellige nivåer. Det er bortkastet, og det kan forvirre deg og få deg til å tro at flere forekomster av det samme problemet oppstod, da en enkelt forekomst i praksis ble logget flere ganger.

Den enkleste måten å gjøre det på er å la alle unntak forplante seg (med mindre de kan håndteres trygt og svelges tidligere) og deretter gjøre logging nær toppnivået av søknaden / systemet ditt.

Sentry

Logging er en evne. Den vanligste implementeringen bruker loggfiler. Men for store distribuerte systemer med hundrevis, tusenvis eller flere servere, er dette ikke alltid den beste løsningen. 

For å holde oversikt over unntak over hele infrastrukturen din, er en tjeneste som sentry super nyttig. Det sentraliserer alle unntaksrapporter, og i tillegg til stabletrappen legger det til tilstanden til hver stakkramme (verdien av variabler da unntaket ble oppdratt). Det gir også et veldig fint grensesnitt med instrumentpaneler, rapporter og måter å bryte ned meldingene ved flere prosjekter. Det er åpen kildekode, så du kan kjøre din egen server eller abonnere på den vertsbaserte versjonen.

Håndtere transient feil

Noen feil er midlertidige, spesielt når det gjelder distribuerte systemer. Et system som freaks ut ved første tegn på problemer er ikke veldig nyttig. 

Hvis koden din har tilgang til noe eksternt system som ikke svarer, er den tradisjonelle løsningen tidsavbrudd, men noen ganger er ikke hvert system designet med timeouts. Timeouts er ikke alltid lett å kalibrere ettersom forholdene endres. 

En annen tilnærming er å svikte fort og prøve igjen. Fordelen er at hvis målet reagerer raskt, trenger du ikke å bruke mye tid i søvntilstand og kan reagere umiddelbart. Men hvis det mislyktes, kan du prøve flere ganger til du bestemmer deg for at det er virkelig uoppnåelig og gi et unntak. I neste avsnitt presenterer jeg en dekoratør som kan gjøre det for deg.

Nyttige dekoratører

To dekoratører som kan hjelpe til med feilhåndtering er @log_error, som logger et unntak og deretter re-reiser det, og @retry dekoratør, som forsøker å ringe en funksjon flere ganger.

Feillogger

Her er en enkel implementering. Dekoratøren unngår et loggerobjekt. Når det pynter en funksjon og funksjonen påberopes, vil den vikle anropet i en prøve-unntaksklausul, og hvis det var et unntak, vil det logge det og til slutt gjenoppta unntaket.

def log_error (logger) def dekorert (f): @ functools.wraps (f) def pakket (* args, ** kwargs): prøv: return f (* args, ** kwargs) unntatt Unntak som e: hvis logger: logger .exception (e) heve retur innpakket retur dekorert

Slik bruker du det:

import logging logger = logging.getLogger () @log_error (logger) def f (): raise Exception ('Jeg er eksepsjonell')

Retrier

Her er en veldig god implementering av @retry dekoratøren.

importtid import matematikk # Retry decorator med eksponentiell backoff def forsøk (prøver, forsinkelse = 3, backoff = 2): "Tilbakestiller en funksjon eller metode til den returnerer True. forsinkelse setter innledende forsinkelse i sekunder, og backoff setter faktoren som forsinkelsen skal forlenge etter hver fiasko. Backoff må være større enn 1, ellers er det egentlig ikke en backoff. Prøvene må være minst 0, og forsinker større enn 0. "hvis 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: hvis rv er sant: # Ferdig på suksess tilbake Sann mtries - = 1 # forbruker et forsøk tid.lei (mdelay) # vent ... mdelay * = backoff # gjør fremtidens vent lenger rv = f (* args, ** kwargs) # Prøv igjen å returnere False # Ran ut av prøver :-( tilbake f_retry # true decorator -> dekorerte funksjonen return deco_retry # @retry (arg [, ...]) -> sann dekoratør

Konklusjon

Feilhåndtering er avgjørende for både brukere og utviklere. Python gir god støtte i språk- og standardbiblioteket for unntaksbasert feilhåndtering. Ved å følge gode metoder flittig, kan du erobre dette ofte forsømte aspektet.

Lær python

Lær Python med vår komplette pythonveiledning, enten du er bare i gang eller du er en erfaren coder som ønsker å lære nye ferdigheter..