Controlla prima vs gestione delle eccezioni?


88

Sto lavorando al libro "Head First Python" (è la mia lingua da imparare quest'anno) e sono arrivato a una sezione in cui discutono di due tecniche di codice:
Verifica prima e Gestione delle eccezioni.

Ecco un esempio del codice Python:

# Checking First
for eachLine in open("../../data/sketch.txt"):
    if eachLine.find(":") != -1:
        (role, lineSpoken) = eachLine.split(":",1)
        print("role=%(role)s lineSpoken=%(lineSpoken)s" % locals())

# Exception handling        
for eachLine in open("../../data/sketch.txt"):
    try:
        (role, lineSpoken) = eachLine.split(":",1)
        print("role=%(role)s lineSpoken=%(lineSpoken)s" % locals())
    except:
        pass

Il primo esempio affronta direttamente un problema nella .splitfunzione. Il secondo consente al gestore delle eccezioni di gestirlo (e ignora il problema).

Discutono nel libro di usare la gestione delle eccezioni invece di controllare prima. L'argomento è che il codice di eccezione rileverà tutti gli errori, in cui il primo controllo prenderà solo le cose a cui pensi (e ti perdi i casi d'angolo). Mi è stato insegnato a controllare prima, quindi il mio istinto iniziale era quello di farlo, ma la loro idea è interessante. Non avevo mai pensato di usare la gestione delle eccezioni per gestire i casi.

Quale delle due è generalmente considerata la migliore pratica?


12
Quella sezione del libro non è intelligente. Se sei in un ciclo e stai gettando eccezioni più e più volte è molto costoso. Ho cercato di delineare alcuni aspetti positivi di quando farlo.
Jason Sebring l'

9
Basta non cadere nella trappola "verifica file esiste". Il file esiste! = Ha accesso al file o che esisterà nei 10 ms necessari per arrivare alla mia chiamata aperta del file, ecc. Blogs.msdn.com/b/jaredpar/archive/2009/04/27/…
Billy ONeal

11
Le eccezioni sono pensate diversamente in Python rispetto ad altre lingue. Ad esempio, il modo di scorrere una raccolta consiste nel chiamare .next () su di esso fino a quando non genera un'eccezione.
WuHoUnited

4
@ emeraldcode.com Questo non è del tutto vero su Python. Non conosco i dettagli, ma la lingua è stata costruita attorno a quel paradigma, quindi il lancio di eccezioni non è così costoso come in altre lingue.
Izkata,

Detto questo, per questo esempio, userei un'istruzione guard: if -1 == eachLine.find(":"): continuequindi il resto del ciclo non verrebbe indentato.
Izkata,

Risposte:


68

In .NET, è pratica comune evitare l'uso eccessivo di Eccezioni. Un argomento è rappresentato dalle prestazioni: in .NET, generare un'eccezione è costoso dal punto di vista computazionale.

Un altro motivo per evitare il loro uso eccessivo è che può essere molto difficile leggere il codice che si basa troppo su di essi. Il blog di Joel Spolsky fa un buon lavoro nel descrivere il problema.

Al centro dell'argomento c'è la seguente citazione:

Il ragionamento è che considero le eccezioni non migliori di "goto", considerate dannose sin dagli anni '60, in quanto creano un brusco salto da un punto di codice a un altro. In realtà sono significativamente peggiori di quelli di goto:

1. Sono invisibili nel codice sorgente . Osservando un blocco di codice, incluse le funzioni che possono o meno generare eccezioni, non c'è modo di vedere quali eccezioni potrebbero essere generate e da dove. Ciò significa che anche un'attenta ispezione del codice non rivela potenziali bug.

2. Creano troppi punti di uscita possibili per una funzione. Per scrivere il codice corretto, devi davvero pensare a ogni possibile percorso del codice attraverso la tua funzione. Ogni volta che chiami una funzione che può sollevare un'eccezione e non rilevarla sul posto, crei opportunità per bug a sorpresa causati da funzioni che si sono interrotte bruscamente, lasciando i dati in uno stato incoerente o altri percorsi di codice che non hai Pensa a.

Personalmente, lancio eccezioni quando il mio codice non può fare ciò per cui è stato contratto. Tendo a usare try / catch quando sto per trattare qualcosa al di fuori del mio limite di processo, ad esempio una chiamata SOAP, una chiamata al database, un file IO o una chiamata di sistema. Altrimenti, provo a programmare in modo difensivo. Non è una regola dura e veloce, ma è una pratica generale.

Scott Hanselman scrive anche delle eccezioni in .NET qui . In questo articolo descrive diverse regole pratiche relative alle eccezioni. Il mio preferito?

Non dovresti fare eccezioni per le cose che accadono continuamente. Quindi sarebbero "ordinari".


5
ecco un altro punto: se la registrazione delle eccezioni è abilitata a livello di applicazione, è meglio usare l'eccezione solo per condizioni eccezionali, non per ordinarie. In caso contrario, il registro verrà ingombra e le vere ragioni che causano errori verranno oscurate.
rwong

2
Bella risposta. Nota, tuttavia, le eccezioni hanno un successo elevato sulla maggior parte delle piattaforme. Tuttavia, come avrai notato con i miei commenti su altre risposte, la performance non è una considerazione nel caso di decidere una regola generale su come codificare qualcosa.
Mattnz,

1
La citazione di Scott Hanselman descrive meglio l'atteggiamento .Net nei confronti delle eccezioni piuttosto che un "uso eccessivo". Le prestazioni sono spesso menzionate, ma il vero argomento è l'inverso del motivo per cui DOVREBBE usare le eccezioni: rende il codice più difficile da capire e da gestire quando una condizione ordinaria si traduce in un'eccezione. Per quanto riguarda Joel, il punto 1 è in realtà un positivo (invisibile significa che il codice mostra ciò che fa, non ciò che non fa), e il punto 2 è irrilevante (sei già in uno stato incoerente o non dovrebbe esserci un'eccezione) . Tuttavia, +1 per "non può fare ciò che gli è stato chiesto di fare".
jmoreno,

5
Sebbene questa risposta vada bene per .Net, non è molto pitonica , quindi dato che si tratta di una domanda su Python, non riesco a capire perché la risposta di Ivc non sia stata votata più.
Mark Booth,

2
@IanGoldby: no. La gestione delle eccezioni è in realtà meglio descritta come recupero delle eccezioni. Se non riesci a ripristinare un'eccezione, probabilmente non dovresti avere alcun codice di gestione delle eccezioni. Se il metodo A chiama il metodo B che chiama C e C genera, molto probabilmente TUTTI A O B dovrebbero recuperare, non entrambi. La decisione "Se non posso fare X farò Y" dovrebbe essere evitata se Y richiede che qualcun altro completi il ​​compito. Se non riesci a completare l'attività, tutto ciò che rimane è la pulizia e la registrazione. La pulizia in .net dovrebbe essere automatica, la registrazione dovrebbe essere centralizzata.
jmoreno,

78

In Python in particolare, di solito è considerata una buona pratica catturare l'eccezione. Tende a essere chiamato Più facile da chiedere perdono che autorizzazione (EAFP), rispetto a Look Before You Leap (LBYL). Ci sono casi in cui LBYL ti darà bug sottili in alcuni casi.

Tuttavia, fai attenzione alle affermazioni nudeexcept: e overbroad tranne le dichiarazioni, poiché entrambi possono anche mascherare i bug - qualcosa del genere sarebbe meglio:

for eachLine in open("../../data/sketch.txt"):
    try:
        role, lineSpoken = eachLine.split(":",1)
    except ValueError:
        pass
    else:
        print("role=%(role)s lineSpoken=%(lineSpoken)s" % locals())

8
Come programmatore .NET, mi arrabbio. Ma poi di nuovo, voi gente fate tutto di strano. :)
Phil

Ciò è estremamente frustrante (gioco di parole non inteso) quando le API non sono coerenti su quali eccezioni vengano generate in quali circostanze o quando più tipi diversi di errori vengono generati sotto lo stesso tipo di eccezione.
Jack,

Quindi finisci per usare lo stesso meccanismo per errori imprevisti e valori di ritorno attesi del tipo. È grande quanto usare 0 come un numero, un falso bool e un puntatore non valido che lascerà il tuo processo con un codice di uscita di 128 + SIGSEGV, perché è conveniente, non hai bisogno di cose diverse ora. Come lo spork! O scarpe con le dita dei piedi ...
yeoman

2
@yeoman quando lanciare un'eccezione è una domanda diversa, si tratta di usare try/ exceptpiuttosto che impostare un condizionale per "è probabile che il seguente lanci un'eccezione", e la pratica di Python preferisce sicuramente la prima. Non fa male che questo approccio sia (probabilmente) più efficiente qui, poiché, nel caso in cui la divisione abbia successo, si cammina la stringa solo una volta. Se splitdovessi lanciare un'eccezione qui, direi che dovrebbe assolutamente - una regola comune è che dovresti lanciare quando non puoi fare quello che dice il tuo nome e non puoi dividere in un delimitatore mancante.
lvc,

Non lo trovo male, lento o terribile, soprattutto perché viene rilevata solo un'eccezione specifica. Ans In realtà mi piace Python. È divertente come a volte non mostri alcun gusto, come ha detto C uso del numero zero, le scarpe preferite di tutti i tempi di Spork e Randall Munroe con le dita dei piedi :) Inoltre, quando sono in Python e un'API dice che questo è il modo per farlo, ci proverò :) Controllare le condizioni in anticipo non è certamente mai una buona idea a causa di concorrenza, coroutine o uno di quelli che vengono aggiunti più avanti ...
yeoman

27

Un approccio pragmatico

Dovresti essere difensivo ma fino a un certo punto. È necessario scrivere la gestione delle eccezioni, ma fino a un certo punto. Userò la programmazione web come esempio perché è qui che vivo.

  1. Supponiamo che tutto l'input dell'utente sia errato e scrivi in ​​modo difensivo solo al punto di verifica del tipo di dati, controlli dei modelli e iniezione dannosa. La programmazione difensiva dovrebbe essere qualcosa che può accadere potenzialmente molto spesso e che non puoi controllare.
  2. Scrivi la gestione delle eccezioni per i servizi di rete che a volte potrebbero non funzionare e gestisci con garbo il feedback degli utenti. La programmazione delle eccezioni dovrebbe essere usata per cose collegate in rete che possono fallire di tanto in tanto ma sono generalmente solide E devi far funzionare il tuo programma.
  3. Non preoccuparti di scrivere in modo difensivo nella tua applicazione dopo aver convalidato i dati di input. È una perdita di tempo e gonfia la tua app. Lascialo esplodere perché è qualcosa di molto raro che non vale la pena maneggiare o significa che devi guardare più attentamente i passaggi 1 e 2.
  4. Non scrivere mai la gestione delle eccezioni nel codice principale che non dipende da un dispositivo in rete. Farlo è una cattiva programmazione e costoso per le prestazioni. Ad esempio, scrivere un try-catch in caso di array fuori limite in un loop significa che non è stato programmato correttamente il loop in primo luogo.
  5. Lascia che tutto sia gestito dalla registrazione degli errori centrale che rileva le eccezioni in un unico posto dopo aver seguito le procedure sopra. Non è possibile rilevare tutti i casi limite poiché potrebbe essere infinito, è sufficiente scrivere codice che gestisca le operazioni previste. Ecco perché usi la gestione centralizzata degli errori come ultima risorsa.
  6. TDD è bello perché in un certo senso ti sta cercando di catturare senza gonfiare, il che significa darti una certa sicurezza del normale funzionamento.
  7. I punti bonus consistono nell'utilizzare uno strumento di copertura del codice, ad esempio Istanbul è una buona opzione per il nodo, poiché questo ti mostra dove non stai testando.
  8. L'avvertenza di tutto ciò sono le eccezioni favorevoli agli sviluppatori . Ad esempio, una lingua verrebbe lanciata se si usasse la sintassi errata e spiegasse il perché. Lo stesso vale per le librerie di utilità da cui dipende la maggior parte del codice.

Ciò deriva dall'esperienza lavorativa in grandi scenari di gruppo.

Un'analogia

Immagina di indossare sempre una tuta spaziale all'interno della ISS. Sarebbe difficile andare in bagno o mangiare, per niente. Sarebbe super voluminoso all'interno del modulo spaziale muoversi. Farebbe schifo. Scrivere un sacco di prove nel tuo codice è un po 'così. Devi avere un punto in cui dici, ehi, ho assicurato che la ISS e i miei astronauti all'interno siano OK, quindi non è pratico indossare una tuta spaziale per ogni scenario che potrebbe accadere.


4
Il problema con Point 3 è che presuppone che il programma e i programmatori che ci lavorano siano perfetti. Non lo sono, quindi è meglio programmare in modo difensivo tenendo presente questi aspetti. Gli importi approssimativi alla giuntura dei tasti possono rendere il software molto più affidabile della mentalità "Se gli input sono controllati tutto perfettamente".
Mattnz,

ecco a cosa servono i test.
Jason Sebring,

3
I test non sono un problema. Devo ancora vedere una suite di test con codice al 100% e copertura "ambientale".
Marjan Venema,

1
@emeraldcode: Vuoi un lavoro con me, mi piacerebbe avere qualcuno nel team che testasse sempre, ad eccezione, ogni permutazione di ogni caso limite che il software potrà mai eseguire. Deve essere bello sapere con certezza abosoluite che il codice è stato testato perfettamente.
Mattnz,

1
Essere d'accordo. Ci sono scenari in cui sia la programmazione difensiva che la gestione delle eccezioni funzionano bene e male, e noi programmatori dovremmo imparare a riconoscerli e scegliere la tecnica più adatta. Mi piace il punto 3 perché credo che dobbiamo assumere, a un certo livello del codice, che alcune condizioni contestuali debbano essere soddisfatte. Queste condizioni sono soddisfatte codificando in modo difensivo lo strato esterno del codice e penso che la gestione delle eccezioni sia adatta quando questi presupposti vengono interrotti nello strato interno.
Yaobin,

15

L'argomento principale del libro è che la versione di eccezione del codice è migliore perché catturerà tutto ciò che potresti aver trascurato se provassi a scrivere il tuo controllo degli errori.

Penso che questa affermazione sia vera solo in circostanze molto specifiche - in cui non ti importa se l'output è corretto.

Non c'è dubbio che sollevare eccezioni è una pratica solida e sicura. Dovresti farlo ogni volta che ritieni che ci sia qualcosa nello stato attuale del programma che tu (come sviluppatore) non puoi o non vuoi affrontare.

Il tuo esempio, tuttavia, riguarda la cattura di eccezioni. Se si cattura un'eccezione, si sta non proteggersi da scenari si potrebbe avere trascurato. Stai facendo esattamente il contrario: supponi di non aver trascurato nessuno scenario che potrebbe aver causato questo tipo di eccezione, e quindi sei sicuro che sia corretto catturarlo (e quindi impedirgli di causare l'uscita dal programma, come qualsiasi eccezione non rilevata).

Utilizzando l'approccio delle eccezioni, se vedi ValueErrorun'eccezione, salti una riga. Usando il tradizionale approccio senza eccezioni, conti il ​​numero di valori restituiti da splite se è inferiore a 2, salti una riga. Dovresti sentirti più sicuro con l'approccio delle eccezioni, dal momento che potresti aver dimenticato alcune altre situazioni di "errore" nel tuo tradizionale controllo degli errori e le except ValueErroravresti prese per te?

Questo dipende dalla natura del tuo programma.

Se stai scrivendo, ad esempio, un browser Web o un lettore video, un problema con gli input non dovrebbe causare un arresto anomalo con un'eccezione non rilevata. È molto meglio produrre qualcosa di sensibilmente remoto (anche se, a rigore, errato) piuttosto che smettere.

Se stai scrivendo un'applicazione in cui la correttezza è importante (come software aziendali o di ingegneria), questo sarebbe un approccio terribile. Se ti sei dimenticato di uno scenario che genera ValueError, la cosa peggiore che puoi fare è ignorare silenziosamente questo scenario sconosciuto e semplicemente saltare la linea. Ecco come i bug molto sottili e costosi finiscono nel software.

Potresti pensare che l'unico modo in cui puoi vedere ValueErrorin questo codice, è se splitrestituito solo un valore (anziché due). Ma cosa succede se la tua printaffermazione in seguito inizia a utilizzare un'espressione che si solleva ValueErrorin alcune condizioni? Questo ti farà saltare alcune linee non perché mancano :, ma perché printfalliscono su di esse. Questo è un esempio di un bug sottile a cui mi riferivo prima: non noteresti nulla, perdi solo alcune righe.

La mia raccomandazione è di evitare di catturare (ma non sollevare!) Eccezioni nel codice in cui produrre output errati è peggio che uscire. L'unica volta in cui rilevo un'eccezione in tale codice è quando ho un'espressione davvero banale, quindi posso facilmente ragionare su ciò che può causare ciascuno dei possibili tipi di eccezione.

Per quanto riguarda l'impatto sulle prestazioni dell'uso delle eccezioni, è banale (in Python) a meno che non si incontrino frequentemente eccezioni.

Se si utilizzano eccezioni per gestire le condizioni che si verificano abitualmente, in alcuni casi è possibile che si paghino costi di prestazioni enormi. Ad esempio, supponiamo di eseguire qualche comando in remoto. È possibile verificare che il testo del comando superi almeno la convalida minima (ad es. Sintassi). Oppure potresti aspettare che venga sollevata un'eccezione (che si verifica solo dopo che il server remoto analizza il tuo comando e trova un problema con esso). Ovviamente, il primo è ordini di grandezza più veloci. Un altro semplice esempio: è possibile verificare se un numero è zero ~ 10 volte più veloce rispetto al tentativo di eseguire la divisione e quindi rilevare l'eccezione ZeroDivisionError.

Queste considerazioni sono importanti solo se si inviano frequentemente stringhe di comando non valide ai server remoti o si ricevono argomenti a valore zero che si utilizzano per la divisione.

Nota: suppongo che useresti al except ValueErrorposto del giusto except; come altri hanno sottolineato, e come dice il libro stesso in poche pagine, non si dovrebbe mai usare nudo except.

Un'altra nota: l'approccio corretto senza eccezioni è quello di contare il numero di valori restituiti split, piuttosto che cercare :. Quest'ultimo è troppo lento, poiché ripete il lavoro svolto splite potrebbe quasi raddoppiare i tempi di esecuzione.


6

Come regola generale, se si conosce che un'istruzione potrebbe generare un risultato non valido, verificarlo e gestirlo. Usa le eccezioni per cose che non ti aspetti; roba "eccezionale". Rende il codice più chiaro in senso contrattuale ("non dovrebbe essere nullo" come esempio).


2

Usa ciò che mai funziona bene in ..

  • il linguaggio di programmazione scelto in termini di leggibilità ed efficienza del codice
  • la tua squadra e il set di convenzioni di codice concordate

Sia la gestione delle eccezioni che la programmazione difensiva sono modi diversi di esprimere lo stesso intento.


0

TBH, non importa se usi il try/exceptmeccanico o un ifcontrollo delle dichiarazioni. Normalmente vedi sia EAFP che LBYL nella maggior parte delle linee di base di Python, con EAFP leggermente più comune. A volte EAFP è molto più leggibile / idiomatico, ma in questo caso particolare penso che vada bene in entrambi i modi.

Però...

Starei attento a usare il tuo riferimento attuale. Un paio di problemi evidenti con il loro codice:

  1. Il descrittore di file è trapelato. Le versioni moderne di CPython (uno specifico interprete Python) lo chiuderanno effettivamente, dal momento che è un oggetto anonimo che rientra nell'ambito del solo ciclo (gc lo annulla dopo il ciclo). Tuttavia, altri interpreti non hanno questa garanzia. Possono fuoriuscire completamente il descrittore. Quasi sempre vuoi usare il withlinguaggio quando leggi file in Python: ci sono pochissime eccezioni. Questo non è uno di questi.
  2. La gestione delle eccezioni di Pokemon è disapprovata mentre maschera gli errori (vale a dire exceptun'istruzione nuda che non rileva un'eccezione specifica)
  3. Nit: non hai bisogno di parentesi per disimballare la tupla. Posso solo farerole, lineSpoken = eachLine.split(":",1)

Ivc ha una buona risposta su questo e EAFP, ma perde anche il descrittore.

La versione LBYL non è necessariamente performante come la versione EAFP, quindi dire che generare eccezioni è "costoso in termini di prestazioni" è categoricamente falso. Dipende molto dal tipo di stringhe che stai elaborando:

In [33]: def lbyl(lines):
    ...:     for line in lines:
    ...:         if line.find(":") != -1:
    ...:             # Nuke the parens, do tuple unpacking like an idiomatic Python dev.
    ...:             role, lineSpoken = line.split(":",1)
    ...:             # no print, since output is obnoxiously long with %timeit
    ...:

In [34]: def eafp(lines):
    ...:     for line in lines:
    ...:         try:
    ...:             # Nuke the parens, do tuple unpacking like an idiomatic Python dev.
    ...:             role, lineSpoken = eachLine.split(":",1)
    ...:             # no print, since output is obnoxiously long with %timeit
    ...:         except:
    ...:             pass
    ...:

In [35]: lines = ["abc:def", "onetwothree", "xyz:hij"]

In [36]: %timeit lbyl(lines)
100000 loops, best of 3: 1.96 µs per loop

In [37]: %timeit eafp(lines)
100000 loops, best of 3: 4.02 µs per loop

In [38]: lines = ["a"*100000 + ":" + "b", "onetwothree", "abconetwothree"*100]

In [39]: %timeit lbyl(lines)
10000 loops, best of 3: 119 µs per loop

In [40]: %timeit eafp(lines)
100000 loops, best of 3: 4.2 µs per loop

-4

Fondamentalmente la gestione delle eccezioni dovrebbe essere più appropriata per le lingue OOP.

Il secondo punto è la prestazione, perché non è necessario eseguire eachLine.findper ogni riga.


7
-1: le prestazioni sono un motivo estremamente scadente per le regole generali.
Mattnz,

3
No, le eccezioni non sono completamente correlate a OOP.
Pubblico

-6

Penso che la programmazione difensiva danneggi le prestazioni. Dovresti anche cogliere solo le eccezioni che gestirai, lasciare che il runtime gestisca l'eccezione che non sai come gestire.


7
Eppure anotehr -1 per preoccuparsi delle prestazioni rispetto alla leggibilità, manutenibilità bla bla bla. Le prestazioni non sono un motivo.
Mattnz,

Posso sapere perché stai andando in giro a distribuire -1 senza spiegare? Programmazione difensiva significa più righe di codice, ciò significa prestazioni peggiori. Qualcuno vuole spiegare prima di ridurre il punteggio?
Manoj

3
@Manoj: A meno che tu non abbia misurato con un profiler e trovato un blocco di codice inaccettabilmente lento, il codice per la leggibilità e la manutenibilità molto prima delle prestazioni.
Daenyth,

Cosa ha detto @Manoj con l'aggiunta che meno codice significa universalmente meno su cui lavorare durante il debug e la manutenzione. Il successo nei tempi degli sviluppatori per qualcosa di meno di quel codice perfetto è estremamente elevato. Suppongo (come me) che non scrivi un codice perfetto, perdonami se sbaglio.
Mattnz,

2
Grazie per il link - Lettura interessante che devo essere d'accordo, fino a un certo punto ... Lavorare su sistemi critici per la vita, come faccio io "Il sistema ha stampato la traccia dello stack, quindi sappiamo esattamente perché quelle 300 persone sono morte inutilmente. .... "non scenderà davvero troppo bene sul banco dei testimoni. Suppongo che sia una di quelle cose in cui ogni situazione ha una risposta appropriata diversa.
mattnz,
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.