Verifica la gestione di First vs Exception?

84

Sto lavorando al libro "Head First Python" (è la mia lingua per imparare quest'anno) e Sono arrivato a una sezione in cui discutono su due tecniche di codice:
Controllo della gestione di First vs Exception.

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 tratta direttamente un problema nella funzione .split . Il secondo consente semplicemente al gestore di eccezioni di gestirlo (e ignora il problema).

Nel libro sostengono di usare la gestione delle eccezioni invece di controllare prima. L'argomento è che il codice di eccezione catturerà tutti gli errori, in cui il primo controllo catturerà 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 dei due è generalmente considerata la pratica migliore?

    
posta jmq 11.03.2012 - 03:53
fonte

9 risposte

64

In .NET, è pratica comune evitare l'uso eccessivo di Eccezioni. Un argomento è la prestazione: in .NET, il lancio di un'eccezione è dispendioso 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 post di blog di Joel Spolsky fa un buon lavoro nel descrivere il problema.

Il fulcro dell'argomento è la seguente citazione:

The reasoning is that I consider exceptions to be no better than "goto's", considered harmful since the 1960s, in that they create an abrupt jump from one point of code to another. In fact they are significantly worse than goto's:

1. They are invisible in the source code. Looking at a block of code, including functions which may or may not throw exceptions, there is no way to see which exceptions might be thrown and from where. This means that even careful code inspection doesn't reveal potential bugs.

2. They create too many possible exit points for a function. To write correct code, you really have to think about every possible code path through your function. Every time you call a function that can raise an exception and don't catch it on the spot, you create opportunities for surprise bugs caused by functions that terminated abruptly, leaving data in an inconsistent state, or other code paths that you didn't think about.

Personalmente, lancio delle eccezioni quando il mio codice non può fare ciò che è stato contrattato. Tendo ad usare try / catch quando sto per gestire 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, cerco di codificare in modo difensivo. Non è una regola dura e veloce, ma è una pratica generale.

Scott Hanselman scrive anche sulle eccezioni in .NET qui . In questo articolo descrive diverse regole empiriche riguardo alle eccezioni. Il mio preferito?

You shouldn't throw exceptions for things that happen all the time. Then they'd be "ordinaries".

    
risposta data 11.03.2012 - 04:42
fonte
76

In Python in particolare, di solito è considerata una pratica migliore per catturare l'eccezione. Tende ad 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 dichiarazioni except: nude e alle dichiarazioni di overbroad escluso , dal momento che entrambi possono anche mascherare bug - qualcosa di simile 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())
    
risposta data 11.03.2012 - 07:06
fonte
25

Un approccio pragmatico

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

  1. Supponiamo che tutti gli input dell'utente siano errati e scrivano in modo difensivo solo al punto di verifica del tipo di dati, controlli di pattern e iniezione dannosa. La programmazione difensiva dovrebbe essere qualcosa che può potenzialmente accadere molto spesso e che non puoi controllare.
  2. Scrivi la gestione delle eccezioni per i servizi di rete che a volte possono fallire e gestire 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 mantenere il tuo programma funzionante.
  3. Non preoccuparti di scrivere in modo difensivo all'interno dell'applicazione dopo che i dati di input sono stati convalidati. È una perdita di tempo e gonfia la tua app. Lascia che esploda perché è qualcosa di molto raro che non vale la pena di gestire o significa che devi guardare più attentamente i passaggi 1 e 2.
  4. Non scrivere mai la gestione delle eccezioni all'interno del codice principale che non dipende da un dispositivo collegato in rete. Fare ciò è una cattiva programmazione e costosa per le prestazioni. Ad esempio, scrivere un try-catch in caso di array fuori limite in un ciclo significa che non hai programmato correttamente il ciclo in primo luogo.
  5. Lascia che tutto venga gestito dalla registrazione degli errori centralizzati che cattura le eccezioni in un posto dopo aver seguito le procedure precedenti. Non è possibile afferrare ogni caso limite in quanto potrebbe essere infinito, è sufficiente scrivere codice che gestisce l'operazione prevista. Ecco perché utilizzi la gestione degli errori centralizzati come ultima risorsa.
  6. Il TDD è bello perché in un certo senso è un tentativo di cattura per te senza gonfiarsi, il che significa garantirti un funzionamento normale.
  7. I punti bonus sono quelli di utilizzare uno strumento di copertura del codice , ad esempio, Istanbul è una buona soluzione per i nodi poiché questo ti mostra dove non stai testando.
  8. L'avvertimento a tutto questo è Eccezioni sviluppatore-friendly . Ad esempio, una lingua verrebbe generata se si utilizza la sintassi errata e si spiega il motivo. Quindi le tue utility librerie da cui dipende la maggior parte del tuo codice.

Questo deriva dall'esperienza lavorativa in scenari di team di grandi dimensioni.

An Analogy

Immagina se indossi una tuta spaziale all'interno della ISS TUTTO il tempo. Sarebbe difficile andare in bagno o mangiare, a tutti. Sarebbe super voluminoso all'interno del modulo spaziale per muoversi. Farebbe schifo. Scrivere un sacco di try-catch all'interno del tuo codice è un po 'come quello. Devi avere un punto in cui dici, hey ho messo al sicuro l'ISS e i miei astronauti all'interno sono OK, quindi non è pratico indossare una tuta spaziale per ogni scenario che potrebbe accadere.

    
risposta data 11.03.2012 - 06:22
fonte
14

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

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

Non c'è dubbio che l'eccezione innalzamento è una pratica sana 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 le eccezioni catching . Se rilevi un'eccezione, sei non che ti protegge dagli scenari che potresti aver trascurato. Stai facendo esattamente il contrario: presumi di non aver trascurato nessuno scenario che potrebbe aver causato questo tipo di eccezione, e quindi sei sicuro che sia giusto prenderlo (e quindi impedire che causi l'uscita del programma, come qualsiasi eccezione non rilevata).

Utilizzando l'approccio dell'eccezione, se vedi l'eccezione ValueError , salti una linea. Utilizzando l'approccio tradizionale non-eccezione, si conta il numero di valori restituiti da split , e se è inferiore a 2, si salta una linea. Dovresti sentirti più sicuro con l'approccio delle eccezioni, dal momento che potresti aver dimenticato alcune altre situazioni di "errore" nel tradizionale controllo degli errori, e except ValueError li prenderebbe per te?

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 il crash di un'eccezione non rilevata. È molto meglio produrre qualcosa di remotamente sensato (anche se, in senso stretto, errato) piuttosto che chiudere.

Se stai scrivendo un'applicazione in cui la correttezza è importante (come il business o il software di progettazione), questo sarebbe un approccio terribile. Se ti sei dimenticato di qualche scenario che aumenta ValueError , la cosa peggiore che puoi fare è ignorare silenziosamente questo scenario sconosciuto e saltare semplicemente la linea. Ecco come finiscono nel software degli errori molto sottili e costosi.

Potresti pensare che l'unico modo in cui puoi vedere ValueError in questo codice, è se split ha restituito solo un valore (invece di due). Ma cosa succede se la tua dichiarazione print in seguito inizia a utilizzare un'espressione che aumenta ValueError in alcune condizioni? Questo ti farà saltare alcune righe non perché manchino : , ma perché print non riesce su di esse. Questo è un esempio di un bug sottile a cui mi riferivo prima - non ti accorgerai di nulla, perdi solo alcune righe.

La mia raccomandazione è di evitare l'intercettazione (ma non il sollevamento!) delle 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 veramente banale, quindi posso facilmente ragionare su cosa potrebbe causare ciascuno dei possibili tipi di eccezione.

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

Se si utilizzano le eccezioni per gestire le condizioni ricorrenti di routine, in alcuni casi è possibile che si paghi un costo elevato per le prestazioni. Ad esempio, supponiamo di eseguire in remoto alcuni comandi. È possibile verificare che il testo del comando superi almeno la convalida minima (ad es. Sintassi). Oppure puoi aspettare che venga sollevata un'eccezione (che accade solo dopo che il server remoto ha analizzato il tuo comando e trovato un problema con esso). Ovviamente, il primo è un ordine di grandezza più veloce. Un altro semplice esempio: puoi verificare se un numero è zero ~ 10 volte più veloce rispetto a provare a eseguire la divisione e quindi a rilevare l'eccezione ZeroDivisionError.

Queste considerazioni sono importanti solo se invii frequentemente stringhe di comandi malformate a server remoti o ricevi argomenti a valore zero che usi per la divisione.

Nota: presumo che useresti except ValueError invece del solo except ; come altri hanno sottolineato, e come dice il libro stesso in poche pagine, non dovresti mai usare except nuda.

Un'altra nota: l'approccio corretto non-eccezione consiste nel contare il numero di valori restituiti da split , piuttosto che cercare : . Quest'ultimo è troppo lento, poiché ripete il lavoro svolto da split e potrebbe quasi raddoppiare il tempo di esecuzione.

    
risposta data 23.04.2013 - 02:54
fonte
6

Come regola generale, se si sa che una dichiarazione potrebbe generare un risultato non valido, testarlo e affrontarlo. Usa le eccezioni per cose che non ti aspetti; roba che è "eccezionale". Rende il codice più chiaro in senso contrattuale ("non dovrebbe essere nullo" come esempio).

    
risposta data 11.03.2012 - 08:58
fonte
2

Usa ciò che funziona bene in ..

  • il tuo linguaggio di programmazione scelto in termini di leggibilità ed efficienza del codice
  • il tuo team e l'insieme delle convenzioni di codice concordate

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

    
risposta data 11.03.2012 - 16:39
fonte
0

TBH, non importa se utilizzi il controllo di try/except meccanico o un% di dichiarazione diif. Solitamente si vedono sia EAFP che LBYL nella maggior parte delle linee di base Python, con EAFP leggermente più comune. A volte EAFP è molto più leggibile / idiomatico, ma in questo caso particolare penso che sia soddisfacente in entrambi i casi.

Tuttavia ...

Farei attenzione a utilizzare il riferimento corrente. Un paio di problemi evidenti con il loro codice:

  1. Il descrittore del file è trapelato. Le versioni moderne di CPython (un interprete Python specifico ) in realtà la chiuderanno, dato che si tratta di un oggetto anonimo che è solo nell'ambito durante il ciclo (gc nuke dopo il ciclo). Tuttavia, altri interpreti non hanno questa garanzia. Potrebbero perdere completamente il descrittore. Quasi sempre si desidera utilizzare l'idioma with durante la lettura dei file in Python: ci sono pochissime eccezioni. Questo non è uno di questi.
  2. La gestione delle eccezioni di Pokemon è disapprovata in quanto maschera gli errori (ad esempio, dichiarazione except nuda che non rileva un'eccezione specifica)
  3. Nit: non hai bisogno di paren per il disimballaggio della tupla. Può solo fare role, lineSpoken = eachLine.split(":",1)

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

La versione LBYL non è necessariamente performante come la versione EAFP, quindi affermare che il lancio di eccezioni è "costoso in termini di prestazioni" è categoricamente falso. Dipende davvero 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
    
risposta data 29.10.2018 - 22:48
fonte
-4

In pratica la gestione delle eccezioni dovrebbe essere più appropriata per i linguaggi OOP.

Il secondo punto è la performance, perché non devi eseguire eachLine.find per ogni riga.

    
risposta data 11.03.2012 - 04:29
fonte
-6

Penso che la programmazione difensiva faccia male alle prestazioni. Dovresti anche prendere solo le eccezioni che dovrai gestire, lasciare che il runtime gestisca l'eccezione che non sai come gestire.

    
risposta data 11.03.2012 - 05:37
fonte

Leggi altre domande sui tag