Le eccezioni per il controllo di flusso sono le migliori pratiche in Python?


15

Sto leggendo "Learning Python" e ho riscontrato quanto segue:

Le eccezioni definite dall'utente possono anche segnalare condizioni di non errore. Ad esempio, una routine di ricerca può essere codificata per sollevare un'eccezione quando viene trovata una corrispondenza invece di restituire un flag di stato per l'interpretazione del chiamante. Di seguito, il gestore eccezioni try / tranne / else svolge il lavoro di un tester del valore restituito if / else:

class Found(Exception): pass

def searcher():
    if ...success...:
        raise Found()            # Raise exceptions instead of returning flags
    else:
        return

Poiché Python è tipizzato in modo dinamico e polimorfico al nucleo, le eccezioni, piuttosto che i valori di ritorno della sentinella, sono il modo generalmente preferito per segnalare tali condizioni.

Ho visto questo genere di cose discusso più volte su vari forum e riferimenti a Python usando StopIteration per terminare i loop, ma non riesco a trovare molto nelle guide di stile ufficiali (PEP 8 ha un riferimento diretto alle eccezioni per il controllo del flusso) o dichiarazioni degli sviluppatori. C'è qualcosa di ufficiale che afferma che questa è la migliore pratica per Python?

Questo ( Le eccezioni come flusso di controllo sono considerate un serio antipattern? Se sì, perché? ) Ha anche diversi commentatori che affermano che questo stile è Pythonic. Su cosa si basa?

TIA

Risposte:


25

Il consenso generale "non usare eccezioni!" Proviene principalmente da altre lingue e anche a volte è obsoleto.

  • In C ++, generare un'eccezione è molto costoso causa dello "svolgersi dello stack". Ogni dichiarazione di variabile locale è come withun'istruzione in Python e l'oggetto in quella variabile può eseguire distruttori. Questi distruttori vengono eseguiti quando viene generata un'eccezione, ma anche al ritorno da una funzione. Questo "linguaggio RAII" è una caratteristica del linguaggio integrale ed è estremamente importante per scrivere codice solido e corretto, quindi RAII contro eccezioni economiche era un compromesso che il C ++ ha deciso verso RAII.

  • All'inizio del C ++, un sacco di codice non veniva scritto in modo sicuro dalle eccezioni: a meno che non si utilizzi effettivamente RAII, è facile perdere la memoria e altre risorse. Quindi, lanciare eccezioni renderebbe quel codice errato. Questo non è più ragionevole poiché anche la libreria standard C ++ usa eccezioni: non si può pretendere che non esistano eccezioni. Tuttavia, le eccezioni sono ancora un problema quando si combina il codice C con C ++.

  • In Java, ogni eccezione ha una traccia dello stack associata. La traccia dello stack è molto utile quando si eseguono errori di debug, ma è uno sforzo sprecato quando l'eccezione non viene mai stampata, ad esempio perché è stata utilizzata solo per il flusso di controllo.

Quindi in quelle lingue le eccezioni sono "troppo costose" per essere utilizzate come flusso di controllo. In Python questo è meno un problema e le eccezioni sono molto più economiche. Inoltre, il linguaggio Python soffre già di alcune spese generali che rendono impercettibile il costo delle eccezioni rispetto ad altri costrutti del flusso di controllo: ad esempio verificare se esiste una voce dict con un test di appartenenza esplicito if key in the_dict: ...è in genere esattamente veloce quanto accedere semplicemente alla voce the_dict[key]; ...e verificare se si ottenere un KeyError. Alcune funzionalità linguistiche integrate (ad es. Generatori) sono progettate in termini di eccezioni.

Pertanto, sebbene non vi siano motivi tecnici per evitare in modo specifico le eccezioni in Python, esiste ancora la questione se si debbano usare al posto dei valori di ritorno. I problemi a livello di progettazione con le eccezioni sono:

  • non sono affatto ovvi. Non puoi facilmente guardare una funzione e vedere quali eccezioni può generare, quindi non sai sempre cosa catturare. Il valore restituito tende ad essere più ben definito.

  • le eccezioni sono il flusso di controllo non locale che complica il tuo codice. Quando si genera un'eccezione, non si sa dove riprenderà il flusso di controllo. Per errori che non possono essere gestiti immediatamente, questa è probabilmente una buona idea, quando notificare al chiamante una condizione è del tutto superflua.

La cultura Python è generalmente inclinata a favore delle eccezioni, ma è facile esagerare. Immagina una list_contains(the_list, item)funzione che controlla se l'elenco contiene un oggetto uguale a quell'elemento. Se il risultato viene comunicato tramite eccezioni che è assolutamente fastidioso, perché dobbiamo chiamarlo così:

try:
  list_contains(invited_guests, person_at_door)
except Found:
  print("Oh, hello {}!".format(person_at_door))
except NotFound:
  print("Who are you?")

Restituire un bool sarebbe molto più chiaro:

if list_contains(invited_guests, person_at_door):
  print("Oh, hello {}!".format(person_at_door))
else:
  print("Who are you?")

Se la funzione dovrebbe già restituire un valore, quindi restituire un valore speciale per condizioni speciali è piuttosto soggetto a errori, perché le persone dimenticheranno di controllare questo valore (questa è probabilmente la causa di 1/3 dei problemi in C). Un'eccezione è generalmente più corretta.

Un buon esempio è una pos = find_string(haystack, needle)funzione che cerca la prima occorrenza della needlestringa nella stringa `pagliaio e restituisce la posizione iniziale. E se la corda del pagliaio non contiene la corda dell'ago?

La soluzione di C e imitata da Python è di restituire un valore speciale. In C questo è un puntatore nullo, in Python lo è -1. Ciò porterà a risultati sorprendenti quando la posizione viene utilizzata come indice di stringa senza controllo, soprattutto perché -1è un indice valido in Python. In C, il puntatore NULL ti darà almeno un segfault.

In PHP, viene restituito un valore speciale di un tipo diverso: il valore booleano FALSEanziché un numero intero. A quanto pare questo non è in realtà migliore a causa delle implicite regole di conversione del linguaggio (ma nota che in Python anche i booleani possono essere usati come ints!). Le funzioni che non restituiscono un tipo coerente sono generalmente considerate molto confuse.

Una variante più solida sarebbe stata quella di lanciare un'eccezione quando non è possibile trovare la stringa, il che assicura che durante il normale flusso di controllo sia impossibile utilizzare accidentalmente il valore speciale al posto di un valore ordinario:

 try:
   pos = find_string(haystack, needle)
   do_something_with(pos)
 except NotFound:
   ...

In alternativa, è possibile utilizzare sempre la restituzione di un tipo che non può essere utilizzato direttamente ma che deve essere prima scartato, ad esempio una tupla booleana di risultati in cui il booleano indica se si è verificata un'eccezione o se il risultato è utilizzabile. Poi:

pos, ok = find_string(haystack, needle)
if not ok:
  ...
do_something_with(pos)

Questo ti costringe a gestire immediatamente i problemi, ma diventa fastidioso molto rapidamente. Ti impedisce anche di concatenare facilmente la funzione. Ogni chiamata di funzione ora richiede tre righe di codice. Golang è un linguaggio che pensa che questo disturbo sia degno della sicurezza.

Quindi, per riassumere, le eccezioni non sono del tutto prive di problemi e possono essere definitivamente abusate, soprattutto quando sostituiscono un valore di ritorno "normale". Ma quando vengono utilizzati per segnalare condizioni speciali (non necessariamente solo errori), le eccezioni possono aiutarti a sviluppare API pulite, intuitive, facili da usare e difficili da usare in modo improprio.


1
Grazie per la risposta approfondita. Vengo da uno sfondo di altre lingue come Java in cui "le eccezioni sono solo per condizioni eccezionali", quindi questo è nuovo per me. Ci sono sviluppatori di core Python che lo hanno mai affermato come hanno fatto con altre linee guida Python come EAFP, EIBTI, ecc. (Su una mailing list, post di blog, ecc.) Vorrei poter citare qualcosa di ufficiale se chiede un altro dev / boss. Grazie!
JB,

Sovraccaricando il metodo fillInStackTrace della tua classe di eccezioni personalizzata per tornare immediatamente immediatamente, puoi renderlo molto economico da rilanciare, poiché lo stack-walking è costoso ed è qui che viene fatto.
John Cowan,

Il tuo primo proiettile nella tua introduzione era inizialmente confuso. Forse potresti cambiarlo in qualcosa come "Il codice di gestione delle eccezioni nel moderno C ++ è a costo zero, ma lanciare un'eccezione è costoso"? (per i lurker: stackoverflow.com/questions/13835817/… ) [modifica: modifica proposta]
phresnel

Si noti che per le operazioni di ricerca del dizionario utilizzando un collections.defaultdicto si my_dict.get(key, default)rende il codice molto più chiaro ditry: my_dict[key] except: return default
Steve Barnes,

11

NO! - non in generale - le eccezioni non sono considerate buone prassi di controllo del flusso ad eccezione della singola classe di codice. L'unico posto in cui le eccezioni sono considerate un modo ragionevole, o addirittura migliore, per segnalare una condizione sono le operazioni del generatore o dell'iteratore. Queste operazioni possono restituire qualsiasi valore possibile come risultato valido, quindi è necessario un meccanismo per segnalare una fine.

Prendi in considerazione la possibilità di leggere un file binario di stream un byte alla volta: assolutamente qualsiasi valore è un risultato potenzialmente valido ma dobbiamo comunque segnalare la fine del file. Quindi abbiamo una scelta, restituire due valori, (il valore byte e un flag valido), ogni volta o sollevare un'eccezione quando non c'è altro da fare. Nei due casi il codice utente può apparire come:

# Using validity flag
valid, val = readbyte(source)
while valid:
    processbyte(val)
    valid, val = readbyte(source)
tidy_up()

in alternativa:

# With exceptions
try:
   val = readbyte(source)
   processbyte(val) # Note if a problem occurs here it will also raise an exception
except Exception: # Use a specific exception here!
   tidy_up()

Ma questo, da quando PEP 343 è stato implementato e portato in backport, tutto è stato accuratamente racchiuso nella withdichiarazione. Quanto sopra diventa, molto pitone:

with open(source) as input:
    for val in input.readbyte(): # This line will raise a StopIteration exception an call input.__exit__()
        processbyte(val) # Not called if there is nothing read

In python3 questo è diventato:

for val in open(source, 'rb').read():
     processbyte(val)

Vi esorto caldamente a leggere PEP 343 che fornisce lo sfondo, la logica, gli esempi, ecc.

È inoltre normale utilizzare un'eccezione per segnalare la fine dell'elaborazione quando si utilizzano le funzioni del generatore per segnalare la fine.

Vorrei aggiungere che il tuo esempio di ricercatore è quasi certamente all'indietro, tali funzioni dovrebbero essere generatori, restituire la prima corrispondenza alla prima chiamata, quindi chiamate sostituenti che restituiscono la partita successiva e sollevare NotFoundun'eccezione quando non ci sono più corrispondenze.


Dici: "NO! - non in generale - le eccezioni non sono considerate buone pratiche di controllo del flusso ad eccezione della singola classe di codice". Qual è la logica di questa affermazione enfatica? Questa risposta sembra concentrarsi strettamente sul caso dell'iterazione. Ci sono molti altri casi in cui le persone potrebbero pensare di usare le eccezioni. Qual è il problema nel farlo in questi altri casi?
Daniel Waltrip,

@DanielWaltrip Vedo troppo codice, in vari linguaggi di programmazione, in cui vengono utilizzate le eccezioni, spesso troppo ampiamente, quando un semplice ifsarebbe meglio. Quando si tratta di debug e test o persino di ragionamento sul comportamento dei codici, è meglio non fare affidamento su eccezioni: dovrebbero essere l'eccezione. Ho visto, nel codice di produzione, un calcolo restituito tramite un lancio / cattura piuttosto che un semplice ritorno, ciò significa che quando il calcolo ha raggiunto un errore di divisione per zero, ha restituito un valore casuale anziché un arresto anomalo nel caso in cui l'errore si sia verificato causando diverse ore di debug.
Steve Barnes,

Ehi, @SteveBarnes, la divisione per zero cattura l'esempio suona come una cattiva programmazione (con una cattura troppo generica che non solleva nuovamente l'eccezione).
wTyeRogers,

La tua risposta sembra ridursi a: A) "NO!" B) esistono esempi validi di casi in cui il flusso di controllo tramite eccezioni funziona meglio delle alternative C) una correzione del codice della domanda per utilizzare i generatori (che utilizzano le eccezioni come flusso di controllo e lo fanno bene). Mentre la tua risposta è generalmente istruttiva, non sembrano esserci argomenti a sostegno dell'articolo A, che, essendo in grassetto e apparentemente la frase principale, mi aspetterei un po 'di supporto. Se fosse supportato, non darei un voto negativo alla tua risposta. Ahimè ...
wTyeRogers il
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.