Gestione degli errori professionale con Python

In questo tutorial imparerai come gestire le condizioni di errore in Python da un punto di vista dell'intero sistema. La gestione degli errori è un aspetto critico del design e passa dai livelli più bassi (a volte l'hardware) fino agli utenti finali. Se non hai una strategia coerente, il tuo sistema sarà inaffidabile, l'esperienza utente sarà scarsa e avrai molte sfide di debug e risoluzione dei problemi. 

La chiave del successo è essere consapevoli di tutti questi aspetti interconnessi, considerandoli esplicitamente e formando una soluzione che indirizzi ogni punto.

Codici di stato rispetto alle eccezioni

Esistono due principali modelli di gestione degli errori: codici di stato ed eccezioni. I codici di stato possono essere utilizzati da qualsiasi linguaggio di programmazione. Le eccezioni richiedono supporto per lingua / runtime. 

Python supporta le eccezioni. Python e la sua libreria standard usano eccezioni liberamente per riferire su molte situazioni eccezionali come errori di I / O, divisione per zero, indicizzazione fuori limite e anche alcune situazioni non eccezionali come la fine dell'iterazione (sebbene sia nascosta). La maggior parte delle biblioteche segue l'esempio e aumenta le eccezioni.

Ciò significa che il tuo codice dovrà gestire le eccezioni sollevate da Python e dalle librerie comunque, quindi potresti anche sollevare eccezioni dal tuo codice quando necessario e non fare affidamento sui codici di stato.

Esempio veloce

Prima di immergerti nelle eccezioni sancta sanction di Python e nelle best practice sulla gestione degli errori, vediamo qualche gestione delle eccezioni in azione:

def f (): return 4/0 def g (): raise Exception ("Non chiamateci. Vi chiameremo") def h (): try: f () tranne Exception as e: print (e) prova: g () tranne Eccezione come e: print (e)

Ecco l'output quando si chiama h ():

h () divisione per zero Non chiamarci. Ti chiameremo

Python Exceptions

Le eccezioni Python sono oggetti organizzati in una gerarchia di classi. 

Ecco l'intera gerarchia:

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

Esistono diverse eccezioni speciali derivate direttamente da BaseException, piace SystemExit, KeyboardInterrupt e GeneratorExit. Poi c'è il Eccezione classe, che è la classe base per StopIteration, Errore standard e avvertimento. Tutti gli errori standard sono derivati ​​da Errore standard.

Quando si alza un'eccezione o qualche funzione chiamata solleva un'eccezione, tale flusso di codice normale termina e l'eccezione inizia a propagare lo stack di chiamate finché non incontra un gestore di eccezioni appropriato. Se nessun gestore di eccezioni è disponibile per gestirlo, il processo (o più esattamente il thread corrente) verrà terminato con un messaggio di eccezione non gestito.

Sollevare le eccezioni

Aumentare le eccezioni è molto semplice. Tu usi semplicemente il aumentare parola chiave per sollevare un oggetto che è una sottoclasse del Eccezione classe. Potrebbe essere un'istanza di Eccezione stessa, una delle eccezioni standard (ad es. RuntimeError) o una sottoclasse di Eccezione ti sei derivato. Ecco un piccolo frammento che illustra tutti i casi:

# Solleva un'istanza della classe Exception solleva Exception ('Ummm ... qualcosa è sbagliato') # Solleva un'istanza della classe RuntimeError raise RuntimeError ('Ummm ... qualcosa è sbagliato') # Solleva una sottoclasse personalizzata di Exception che mantiene il timestamp l'eccezione è stata creata dalla classe datetime import di datetime SuperError (Exception): def __init __ (self, message): Exception .__ init __ (message) self.when = datetime.now () genera SuperError ('Ummm ... something is wrong')

Cattura le eccezioni

Prendi eccezioni con il tranne clausola, come hai visto nell'esempio. Quando rilevi un'eccezione, hai tre opzioni:

  • Inghiottirlo tranquillamente (maneggiarlo e continuare a correre).
  • Fai qualcosa come la registrazione, ma controrilancia la stessa eccezione per lasciar gestire i livelli più alti.
  • Alza un'eccezione diversa anziché l'originale.

Ingoiare l'eccezione

Dovresti ingoiare l'eccezione se sai come gestirlo e puoi recuperarlo completamente. 

Ad esempio, se si riceve un file di input che può essere in diversi formati (JSON, YAML), è possibile provare a analizzarlo utilizzando parser diversi. Se il parser JSON ha sollevato un'eccezione che il file non è un file JSON valido, lo si inghiottisce e si prova con il parser YAML. Se il parser YAML ha fallito anche tu hai lasciato che l'eccezione si propagasse.

import json import yaml def parse_file (nome file): try: return json.load (open (filename)) eccetto json.JSONDecodeError return yaml.load (open (filename))

Tieni presente che altre eccezioni (ad esempio il file non trovato o nessuna autorizzazione di lettura) si propagheranno e non verranno catturate dalla specifica clausola except. Questa è una buona politica in questo caso in cui si desidera provare l'analisi YAML solo se l'analisi JSON non è riuscita a causa di un problema di codifica JSON. 

Se vuoi gestire tutti eccezioni quindi basta usare tranne Eccezione. Per esempio:

def print_exception_type (func, * args, ** kwargs): try: return func (* args, ** kwargs) tranne Exception come e: print type (e)

Si noti che aggiungendo come e, si lega l'oggetto eccezione al nome e disponibile nella tua clausola except.

Ri-aumentare la stessa eccezione

Per controrilanciare, basta aggiungere aumentare senza argomenti all'interno del tuo gestore. Ciò consente di eseguire alcune operazioni locali, ma consente comunque ai livelli superiori di gestirle. Qui, il invoke_function () funzione stampa il tipo di eccezione alla console e quindi solleva nuovamente l'eccezione.

def invoke_function (func, * args, ** kwargs): try: return func (* args, ** kwargs) tranne Exception come e: print type (e) raise

Alza un'eccezione diversa

Esistono diversi casi in cui si desidera sollevare un'eccezione diversa. A volte si desidera raggruppare più eccezioni di basso livello in una singola categoria gestita in modo uniforme dal codice di livello superiore. In casi di ordine, è necessario trasformare l'eccezione a livello di utente e fornire un contesto specifico per l'applicazione. 

Finalmente clausola

A volte si desidera assicurarsi che venga eseguito qualche codice di pulizia anche se è stata sollevata un'eccezione lungo il percorso. Ad esempio, potresti avere una connessione al database che vuoi chiudere quando hai finito. Ecco il modo sbagliato per farlo:

def fetch_some_data (): db = open_db_connection () query (db) close_db_Connection (db)

Se la query () funzione solleva un'eccezione quindi la chiamata a close_db_connection () non verrà mai eseguito e la connessione DB rimarrà aperta. Il finalmente la clausola viene sempre eseguita dopo un tentativo che viene eseguito tutto il gestore di eccezioni. Ecco come farlo correttamente:

def fetch_some_data (): db = Nessuno prova: db = open_db_connection () query (db), infine: se db non è None: close_db_connection (db)

La chiamata a open_db_connection () potrebbe non restituire una connessione o sollevare un'eccezione stessa. In questo caso non è necessario chiudere la connessione DB.

Quando si usa finalmente, bisogna stare attenti a non sollevare eccezioni perché maschereranno l'eccezione originale.

Gestori di contesto

I gestori di contesto forniscono un altro meccanismo per avvolgere risorse come file o connessioni DB nel codice di pulizia che viene eseguito automaticamente anche quando sono state sollevate eccezioni. Invece dei blocchi try-finally, usi il con dichiarazione. Ecco un esempio con un file:

def process_file (filename): con open (nomefile) come f: process (f.read ()) 

Ora, anche se processi() sollevata un'eccezione, il file verrà chiuso correttamente immediatamente quando lo scopo del con il blocco viene chiuso, indipendentemente dal fatto che l'eccezione sia stata gestita o meno.

Registrazione

La registrazione è praticamente un requisito in sistemi non banali e di lunga durata. È particolarmente utile nelle applicazioni Web in cui è possibile trattare tutte le eccezioni in modo generico: basta registrare l'eccezione e restituire un messaggio di errore al chiamante. 

Durante la registrazione, è utile registrare il tipo di eccezione, il messaggio di errore e lo stacktrace. Tutte queste informazioni sono disponibili tramite sys.exc_info oggetto, ma se usi il logger.exception () metodo nel tuo gestore di eccezioni, il sistema di registrazione Python estrae tutte le informazioni rilevanti per te.

Questa è la migliore pratica che consiglio:

logger di registrazione di importazione = logging.getLogger () def f (): try: flaky_func () eccetto Exception: logger.exception () raise

Se segui questo schema (supponendo che tu abbia configurato correttamente la registrazione), non importa cosa succederà, avrai una buona registrazione nei tuoi registri di cosa è andato storto e sarai in grado di risolvere il problema.

Se controrilanciate, assicuratevi di non registrare più volte la stessa eccezione a diversi livelli. È uno spreco e potrebbe confondervi e farvi pensare a più istanze dello stesso problema, quando in pratica una singola istanza è stata registrata più volte.

Il modo più semplice per farlo è lasciare che tutte le eccezioni si propagino (a meno che non possano essere gestite in modo sicuro e ingoiate in precedenza) e quindi fare il logging vicino al livello più alto della tua applicazione / sistema.

Sentinella

La registrazione è una capacità. L'implementazione più comune è l'utilizzo dei file di registro. Ma, per sistemi distribuiti su larga scala con centinaia, migliaia o più server, questa non è sempre la soluzione migliore. 

Per tenere traccia delle eccezioni sull'intera infrastruttura, un servizio come sentinella è di grande aiuto. Centralizza tutti i report delle eccezioni e, oltre allo stacktrace, aggiunge lo stato di ogni frame dello stack (il valore delle variabili nel momento in cui è stata sollevata l'eccezione). Fornisce anche un'interfaccia davvero piacevole con dashboard, report e modi per abbattere i messaggi di più progetti. È open source, quindi puoi eseguire il tuo server o iscriverti alla versione ospitata.

Affrontare il fallimento transitorio

Alcuni guasti sono temporanei, in particolare quando si tratta di sistemi distribuiti. Un sistema che impazzisce al primo segno di problemi non è molto utile. 

Se il tuo codice accede a un sistema remoto che non risponde, la soluzione tradizionale è timeout, ma a volte non tutti i sistemi sono progettati con timeout. I timeout non sono sempre facili da calibrare al variare delle condizioni. 

Un altro approccio è fallire velocemente e poi riprovare. Il vantaggio è che se l'obiettivo risponde velocemente, non è necessario passare molto tempo in condizioni di sonno e reagire immediatamente. Ma se fallisce, puoi riprovare più volte finché non decidi che è davvero irraggiungibile e genera un'eccezione. Nella prossima sezione, presenterò un decoratore che può farlo per te.

Decoratori utili

Due decoratori che possono aiutare con la gestione degli errori sono i @log_error, che registra un'eccezione e quindi la rilancia, e il @retry decoratore, che riproverà a chiamare una funzione più volte.

Error Logger

Ecco una semplice implementazione. Il decoratore esclude un oggetto logger. Quando decora una funzione e la funzione viene invocata, la chiamata verrà racchiusa in una clausola try-except, e se c'è un'eccezione la registrerà e infine rileverà l'eccezione.

def log_error (logger) def decorato (f): @ functools.wraps (f) def wrapped (* args, ** kwargs): try: return f (* args, ** kwargs) tranne Exception come e: se logger: logger .exception (e) alza il reso con ritorno reso decorato

Ecco come usarlo:

logger di registrazione di importazione = logging.getLogger () @log_error (logger) def f (): raise Exception ('I am exceptional')

Retrier

Ecco una buona implementazione del decoratore @retry.

import time import math # Retry decorator con backoff esponenziale retry (try, delay = 3, backoff = 2): "Ritenta una funzione o un metodo finché non restituisce True. delay imposta il ritardo iniziale in secondi e backoff imposta il fattore di il ritardo dovrebbe allungarsi dopo ogni errore, il backoff deve essere maggiore di 1, altrimenti non è realmente un backoff, i tentativi devono essere almeno 0 e ritardare maggiore di 0. "se 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: se rv è True: # Fine in caso di esito positivo True mtries - = 1 # consuma un tentativo time.sleep (mdelay) # wait ... mdelay * = backoff # make future wait longer rv = f (* args, ** kwargs) # Prova di nuovo a restituire False # Esaurito dai tentativi :-( return f_retry # true decorator -> funzione decorata return deco_retry # @retry (arg [, ...]) -> true decorator

Conclusione

La gestione degli errori è fondamentale sia per gli utenti che per gli sviluppatori. Python fornisce un grande supporto nella lingua e nella libreria standard per la gestione degli errori basata su eccezioni. Seguendo diligentemente le migliori pratiche, puoi conquistare questo aspetto spesso trascurato.

Impara Python

Impara Python con la nostra guida completa al tutorial su Python, sia che tu stia appena iniziando o che sei un programmatore esperto che cerca di imparare nuove abilità.