Quando e come devo usare le eccezioni?

19

L'impostazione

Spesso ho problemi a determinare quando e come usare le eccezioni. Consideriamo un semplice esempio: supponiamo che io scriva una pagina web, dica " link ", per determinare se Abe Vigoda è ancora vivo. Per fare questo, tutto ciò che dobbiamo fare è scaricare la pagina e cercare le volte che appare la frase "Abe Vigoda". Restituiamo la prima apparizione, dal momento che include lo stato di Abe. Concettualmente, assomiglierà 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 argomentare che ci sono modi migliori e più robusti per raschiare questa pagina per lo stato di Abe, ricorda che questo è solo un esempio semplice e forzato usato per evidenziare una situazione comune in cui mi trovo.

Ora, dove questo codice può incontrare dei problemi? Tra gli altri errori, alcuni "previsti" sono:

  • download_page potrebbe non essere in grado di scaricare la pagina e lancia un IOError .
  • L'URL potrebbe non puntare alla pagina giusta, oppure la pagina viene scaricata in modo errato e quindi non ci sono hit. hits è la lista vuota, quindi.
  • La pagina web è stata alterata, forse rendendo errate le nostre supposizioni sulla pagina. Forse ci aspettiamo 4 menzioni di Abe Vigoda, ma ora 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: viene generata una IOError e può essere gestita dal chiamante della mia funzione. Quindi consideriamo gli altri casi e come potrei gestirli. Ma prima, supponiamo di implementare parse_abe_status nel modo più stupido possibile:

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

Ovvero, non esegue alcun controllo degli errori. Ora, sulle 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 None dalla mia funzione, dovrebbe presumere che non ci fossero menzioni di Abe Vigoda, e quindi qualcosa è andato storto. Ma questo è abbastanza vago, giusto? E non aiuta il caso in cui hits[0] non è quello che pensavamo fosse.

D'altra parte, possiamo aggiungere alcune eccezioni:

Opzione 2: utilizzo delle eccezioni

Se hits è vuoto, verrà generata una IndexError quando tentiamo hits[0] . Ma non ci si deve aspettare che il chiamante gestisca un IndexError generato dalla mia funzione, dal momento che non ha idea di dove sia venuto il IndexError ; potrebbe essere stato lanciato da find_all_mentions , per quanto 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"

Ora cosa succede se la pagina è cambiata e c'è un numero imprevisto di colpi? Questo non è catastrofico, in quanto il codice potrebbe ancora funzionare, ma un chiamante potrebbe desiderare di essere extra attento, o potrebbe voler registrare un avviso. 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 status non è vivo o morto. Forse, per qualche strana ragione, oggi si è rivelato essere comatose . Quindi non voglio restituire False , poiché ciò implica che Abe è morto. Cosa dovrei fare qui? Fai un'eccezione, probabilmente. Ma che tipo? Devo creare una classe di eccezioni 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 in mezzo

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

    
posta jme 23.11.2013 - 16:00
fonte

4 risposte

16

La raccomandazione in Python è di usare le eccezioni per indicare il fallimento. Questo è vero anche se si prevede un errore su base regolare.

Guardalo dal punto di vista del chiamante del tuo codice:

my_status = get_abe_status(my_url)

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

E se sollevassimo un'eccezione? Se il chiamante non gestisce specificamente il caso, l'eccezione si propagherà verso l'alto, raggiungendo infine il gestore di eccezioni predefinito. Potrebbe non essere quello che vuoi, ma è meglio che introdurre un bug sottile altrove nel programma. Inoltre, l'eccezione fornisce informazioni su cosa è andato storto che è stato perso nella prima versione.

Dal punto di vista del chiamante, è semplicemente più conveniente ottenere un'eccezione che un valore di ritorno. E questo è lo stile Python, per usare le eccezioni per indicare che le condizioni di errore non restituiscono 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 corso non dovrebbe sollevare eccezioni. Una ragione che viene data per questo è che le eccezioni sono grossolanamente inefficienti, ma in realtà 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 verificare la presenza di una lista vuota. 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 correttamente?

    
risposta data 08.12.2013 - 01:27
fonte
4

La risposta accettata merita di essere accettata e risponde alla domanda, la scrivo solo per fornire un po 'di background extra.

Uno dei creduti di Python è: è più facile chiedere perdono che il permesso. Ciò significa che tipicamente fai semplicemente le cose, e se ti aspetti eccezioni, le gestisci. Invece di fare se controlli in anticipo per assicurarti di non ottenere un'eccezione.

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

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

Un modo per pensarci: accedere a myvector[k] dove k > = myvector.size () causerà un'eccezione. Quindi in linea di principio potresti scrivere questo (molto imbarazzante) come un tentativo di cattura.

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

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

for i in range(1):
    ...

Come funziona? Il ciclo for prende il risultato dell'intervallo (1) e chiama iter () su di esso, afferrandolo con un iteratore.

b = range(1).__iter__()

Quindi chiama su di esso ad ogni iterazione del ciclo, fino a ...:

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

In altre parole, un loop for in python è in realtà un tentativo, tranne che sotto mentite spoglie.

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

Nel caso del tuo particolare codice, direi che qualsiasi situazione che causa il successo di una lista vuota dovrebbe essere lanciata. Perché? Bene, il modo in cui la tua funzione è impostata, non c'è modo di determinare la risposta senza analizzare i colpi. Quindi, se gli hit non sono analizzabili, perché l'URL è sbagliato o perché gli hit sono vuoti, la funzione non può rispondere alla domanda e in effetti non può nemmeno 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 None, non ci sarà un errore, sarà semplicemente trattato come False. Quindi, il tuo cliente sarà fondamentalmente sempre costretto a fare un controllo se None è in ogni caso, se non vuole fallimenti silenziosi ... quindi probabilmente dovresti solo lanciare.

    
risposta data 27.09.2014 - 20:20
fonte
2

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

Se, d'altra parte, ti aspetti davvero che i consumatori del tuo metodo siano sempre (a meno che non si rovinino in qualche modo) trovi ciò che viene cercato, allora non trovarlo sarebbe un'eccezione e dovresti accettarlo.

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 ad esempio una traccia dello stack, al fine di aiutare la gente a decifrare il motivo della loro comparsa. Non penso che sia quello che stai cercando di fare.

    
risposta data 23.11.2013 - 18:14
fonte
2

Se stavo scrivendo una funzione

 def abe_is_alive():

Scriverò in return True o False nei casi in cui sono assolutamente certo dell'uno o dell'altro e raise un errore in qualsiasi altro caso (ad esempio raise ValueError("Status neither 'dead' nor 'alive'") ). Questo perché la funzione che chiama il mio è in attesa di un valore booleano, e se non posso fornire con certezza che il flusso regolare del programma non dovrebbe continuare.

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

Invece di

try:
    hits[0] 
except IndexError:
    raise NotFoundError

Vorrei verificare esplicitamente:

if not hits:
    raise NotFoundError

in quanto tende ad essere "più economico", quindi imposta try .

Sono d'accordo con te su IOError ; Inoltre, non proverei a gestire l'errore durante la connessione al sito Web - se non possiamo, per qualche ragione, questo non è il luogo appropriato per gestirlo (in quanto non ci aiuta a rispondere alla nostra domanda) e dovrebbe passare fuori alla funzione chiamante.

    
risposta data 08.12.2013 - 00:37
fonte

Leggi altre domande sui tag