Quando e come dovrei usare le eccezioni?


20

Le impostazioni

Spesso ho difficoltà a determinare quando e come utilizzare le eccezioni. Consideriamo un semplice esempio: supponiamo che stia raschiando una pagina Web, ad esempio " http://www.abevigoda.com/ ", per determinare se Abe Vigoda è ancora vivo. Per fare ciò, tutto ciò che dobbiamo fare è scaricare la pagina e cercare le volte in cui appare la frase "Abe Vigoda". Restituiamo la prima apparizione, poiché include lo status di Abe. Concettualmente, sarà simile a questo:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Dove parse_abe_status(s)prende una stringa del modulo "Abe Vigoda è qualcosa " e restituisce la parte " qualcosa ".

Prima di sostenere che ci sono modi molto migliori e più robusti per eliminare questa pagina per lo stato di Abe, ricorda che questo è solo un esempio semplice e inventato utilizzato per evidenziare una situazione comune in cui mi trovo.

Ora, dove possono verificarsi problemi questo codice? Tra gli altri errori, alcuni "previsti" sono:

  • download_pagepotrebbe non essere in grado di scaricare la pagina e genera un IOError.
  • L'URL potrebbe non puntare alla pagina giusta oppure la pagina viene scaricata in modo errato, quindi non ci sono hit. hitsè l'elenco vuoto, quindi.
  • La pagina Web è stata modificata, probabilmente rendendo errati i nostri presupposti sulla pagina. Forse ci aspettiamo 4 menzioni di Abe Vigoda, ma ora ne troviamo 5.
  • Per alcuni motivi, hits[0]potrebbe non essere una stringa del modulo "Abe Vigoda è qualcosa ", e quindi non può essere analizzato correttamente.

Il primo caso non è davvero un problema per me: IOErrorviene generato e può essere gestito dal chiamante della mia funzione. Quindi consideriamo gli altri casi e come potrei gestirli. Ma prima, supponiamo che implementiamo parse_abe_statusnel modo più stupido possibile:

def parse_abe_status(s):
    return s[13:]

Vale a dire, non esegue alcun controllo degli errori. Ora, per le opzioni:

Opzione 1: ritorno None

Posso dire al chiamante che qualcosa è andato storto restituendo None:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    if not hits:
        return None

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Se il chiamante riceve Nonedalla mia funzione, dovrebbe presumere che non ci siano menzioni di Abe Vigoda, e quindi qualcosa è andato storto. Ma questo è piuttosto vago, giusto? E non aiuta il caso in cui hits[0]non è quello che pensavamo fosse.

D'altra parte, possiamo mettere alcune eccezioni:

Opzione 2: utilizzo delle eccezioni

Se hitsè vuoto, IndexErrorverrà lanciato un testamento quando tentiamo hits[0]. Ma non ci si dovrebbe aspettare che il chiamante gestisca una IndexErrormia funzione, dal momento che non ha idea di da dove IndexErrorprovenga; avrebbe potuto essere lanciato da find_all_mentions, per quel che ne sa. Quindi creeremo una classe di eccezioni personalizzata per gestire questo:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

E se la pagina fosse cambiata e ci fosse un numero inaspettato di hit? Questo non è catastrofica, in quanto il codice può ancora funzionare, ma il chiamante potrebbe desiderare di essere più attenti, o lui potrebbe voler registrare un avvertimento. Quindi lancerò un avvertimento:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Infine, potremmo scoprire che statusnon è né vivo né morto. Forse, per qualche strana ragione, oggi si è rivelato essere comatose. Quindi non voglio tornare False, poiché ciò implica che Abe è morto. Cosa dovrei fare qui? Lancia un'eccezione, probabilmente. Ma che tipo? Devo creare una classe di eccezione personalizzata?

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    if status not in ['alive', 'dead']:
        raise SomeTypeOfError("Status is an unexpected value.")

    # he's either alive or dead
    return status == "alive"

Opzione 3: da qualche parte nel mezzo

Penso che il secondo metodo, con le eccezioni, sia preferibile, ma non sono sicuro di utilizzare correttamente le eccezioni al suo interno. Sono curioso di vedere come programmatori più esperti potrebbero gestire questo.

Risposte:


17

La raccomandazione in Python è quella di utilizzare le eccezioni per indicare l'errore. Questo è vero anche se ti aspetti un fallimento su base regolare.

Guardalo dal punto di vista del chiamante del tuo codice:

my_status = get_abe_status(my_url)

Cosa succede se restituiamo Nessuno? Se il chiamante non gestisce specificamente il caso in cui get_abe_status non è riuscito, proverà semplicemente a continuare con my_stats su None. Ciò potrebbe produrre un bug difficile da diagnosticare in seguito. Anche se controlli Nessuno, questo codice non ha idea del perché get_abe_status () non è riuscito.

E se sollevassimo un'eccezione? Se il chiamante non gestisce in modo specifico il caso, l'eccezione si propaga verso l'alto alla fine colpendo il gestore delle eccezioni predefinito. Potrebbe non essere quello che vuoi, ma è meglio introdurre un bug sottile altrove nel programma. Inoltre, l'eccezione fornisce informazioni su ciò che è andato storto e che si perde nella prima versione.

Dal punto di vista del chiamante, è semplicemente più conveniente ottenere un'eccezione rispetto a un valore di ritorno. E questo è lo stile Python, per utilizzare le eccezioni per indicare condizioni di errore e non restituire valori.

Alcuni prenderanno una prospettiva diversa e sostengono che dovresti usare le eccezioni solo per i casi che non ti aspetti mai realmente. Sostengono che normalmente la corsa in esecuzione non dovrebbe sollevare eccezioni. Uno dei motivi che viene dato per questo è che le eccezioni sono gravemente inefficienti, ma ciò non è vero per Python.

Un paio di punti sul tuo codice:

try:
    hits[0]
except IndexError:
    raise NotFoundError("No mentions found.")

Questo è un modo davvero confuso per cercare un elenco vuoto. Non indurre un'eccezione solo per controllare qualcosa. Usa un if.

# say we expect four hits...
if len(hits) != 4:
    raise Warning("An unexpected number of hits.")
    logger.warning("An unexpected number of hits.")

Ti rendi conto che la linea logger.warning non funzionerà mai bene?


1
Grazie (in ritardo) per la tua risposta. Oltre a guardare il codice pubblicato, ha migliorato il mio feeling su quando e come generare un'eccezione.
JME

4

La risposta accettata merita di essere accettata e risponde alla domanda, scrivo questo solo per fornire un po 'di sfondo in più.

Uno dei punti di forza di Python è: è più facile chiedere perdono che autorizzazione. Ciò significa che in genere fai solo le cose e, se ti aspetti eccezioni, le gestisci. Al contrario di fare se controlli in anticipo per assicurarsi che non otterrai un'eccezione.

Voglio fornire un esempio per mostrarti quanto sia drammatica la differenza nella mentalità da C ++ / Java. Un ciclo for in C ++ in genere assomiglia a:

for(int i = 0; i != myvector.size(); ++i) ...

Un modo di pensarci: l'accesso a myvector[k]dove k> = myvector.size () provocherà un'eccezione. Quindi, in linea di principio, potresti scriverlo (in modo molto imbarazzante) come un trucchetto.

    for(int i = 0; ; ++i)  {
        try {
           ...
        } catch (& std::out_of_range)
             break

O qualcosa di simile. Ora, considera cosa sta succedendo in un pitone per loop:

for i in range(1):
    ...

Come funziona? Il ciclo for prende il risultato di range (1) e chiama iter () su di esso, afferrandogli un iteratore.

b = range(1).__iter__()

Quindi lo chiama successivamente ad ogni iterazione di loop, fino a ...:

>>> next(b)
0
>>> next(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

In altre parole, un ciclo for in Python è in realtà un tentativo-tranne sotto mentite spoglie.

Per quanto riguarda la domanda concreta, ricorda che le eccezioni bloccano la normale esecuzione delle funzioni e devono essere trattate separatamente. In Python, dovresti lanciarli liberamente ogni volta che non ha senso eseguire il resto del codice nella tua funzione e / o nessuno dei ritorni riflette correttamente ciò che è accaduto nella funzione. Nota che tornare presto da una funzione è diverso: tornare presto significa che hai già capito la risposta e non hai bisogno del resto del codice per capire la risposta. Sto dicendo che dovrebbero essere generate eccezioni quando la risposta non è nota e il resto del codice per determinare la risposta non può essere ragionevolmente eseguito. Ora, "riflettere correttamente" stesso, come le eccezioni che si sceglie di lanciare, è tutta una questione di documentazione.

Nel caso del tuo particolare codice, direi che qualsiasi situazione che causa hit in un elenco vuoto dovrebbe essere lanciata. Perché? Bene, il modo in cui è impostata la tua funzione, non c'è modo di determinare la risposta senza analizzare i risultati. Quindi, se gli hit non sono analizzabili, sia perché l'URL è errato, sia perché gli hit sono vuoti, la funzione non può rispondere alla domanda e in effetti non può nemmeno davvero provarci.

In questo caso particolare, direi che anche se riesci ad analizzare e non ottieni una risposta ragionevole (viva o morta), dovresti comunque lanciare. Perché? Perché, la funzione restituisce un valore booleano. Restituire None è molto pericoloso per il tuo cliente. Se eseguono un controllo if su Nessuno, non si verificherà alcun errore, verrà silenziosamente trattato come Falso. Quindi, il tuo cliente dovrà praticamente sempre fare un controllo if is None in ogni caso se non vuole fallimenti silenziosi ... quindi probabilmente dovresti semplicemente lanciare.


2

Dovresti usare le eccezioni quando si verifica qualcosa di eccezionale . Cioè, qualcosa che non dovrebbe accadere dato l'uso corretto dell'applicazione. Se è consentito e previsto per il consumatore del tuo metodo cercare qualcosa che non verrà trovato, "non trovato" non è un caso eccezionale. In questo caso, è necessario restituire null o "None" o {} o qualcosa che indica un set di restituzione vuoto.

Se, d'altra parte, ti aspetti davvero che i consumatori del tuo metodo trovino sempre (a meno che non abbiano sbagliato in qualche modo) quello che stai cercando, allora non trovarlo sarebbe un'eccezione e dovresti andare con quello.

La chiave è che la gestione delle eccezioni può essere costosa: si suppone che le eccezioni raccolgano informazioni sullo stato dell'applicazione quando si verificano, come una traccia dello stack, per aiutare le persone a decifrare il motivo per cui si sono verificate. Non penso sia quello che stai cercando di fare.


1
Se decidi che non è possibile trovare un valore, fai attenzione a ciò che usi per indicare che è successo. Se il tuo metodo dovrebbe restituire a Stringe scegli "Nessuno" come indicatore, ciò significa che devi fare attenzione che "Nessuno" non sia mai un valore valido. Si noti inoltre che esiste una differenza tra guardare i dati e non trovare un valore e non essere in grado di recuperare i dati, quindi non possiamo trovare i dati. Avere lo stesso risultato per questi due casi significa che non si ha visibilità quando non si ottiene alcun valore quando ci si aspetta che ce ne sia uno.
unholysampler il

I blocchi di codice in linea sono contrassegnati da backtick (`), forse è quello che intendevi fare con il" Nessuno "?
Izkata,

3
Temo che ciò sia assolutamente falso in Python. Stai applicando il ragionamento in stile C ++ / Java in un'altra lingua. Python usa le eccezioni per indicare la fine di un ciclo for; è piuttosto ineccepibile.
Nir Friedman,

2

Se stavo scrivendo una funzione

 def abe_is_alive():

Lo scriverei return Trueo Falsenei casi in cui sono assolutamente certo dell'uno o dell'altro, e raiseun errore in ogni altro caso (ad es raise ValueError("Status neither 'dead' nor 'alive'").). Questo perché la funzione che chiama mia è in attesa di un valore booleano e, se non posso fornirlo con certezza, il normale flusso del programma non dovrebbe continuare.

Qualcosa come il tuo esempio di ottenere un numero diverso di "hit" del previsto, probabilmente lo ignorerei; fintanto che uno dei successi corrisponde ancora al mio modello "Abe Vigoda è {morto | vivo}", va bene. Ciò consente di riorganizzare la pagina ma ottiene comunque le informazioni appropriate.

Piuttosto che

try:
    hits[0] 
except IndexError:
    raise NotFoundError

Verificherei esplicitamente:

if not hits:
    raise NotFoundError

poiché tende ad essere "più economico", quindi impostare il try.

Sono d'accordo con te su IOError; Inoltre, non tenterei di gestire l'errore di connessione al sito Web - se non potessimo, per qualche motivo, questo non è il posto adatto per gestirlo (poiché non ci aiuta a rispondere alla nostra domanda) e dovrebbe passare alla funzione di chiamata.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.