È una buona pratica usare try-tranne-else in Python?


440

Di tanto in tanto in Python, vedo il blocco:

try:
   try_this(whatever)
except SomeException as exception:
   #Handle exception
else:
   return something

Qual è il motivo per cui esiste try-tranne-else?

Non mi piace questo tipo di programmazione, poiché utilizza le eccezioni per eseguire il controllo del flusso. Tuttavia, se è incluso nella lingua, ci deve essere una buona ragione, vero?

Comprendo che le eccezioni non sono errori e che dovrebbero essere utilizzate solo per condizioni eccezionali (ad esempio, provo a scrivere un file sul disco e non c'è più spazio o forse non ho l'autorizzazione) e non per il flusso controllo.

Normalmente gestisco le eccezioni come:

something = some_default_value
try:
    something = try_this(whatever)
except SomeException as exception:
    #Handle exception
finally:
    return something

O se davvero non voglio restituire nulla se si verifica un'eccezione, allora:

try:
    something = try_this(whatever)
    return something
except SomeException as exception:
    #Handle exception

Risposte:


667

"Non so se sia per ignoranza, ma non mi piace quel tipo di programmazione, poiché utilizza le eccezioni per eseguire il controllo del flusso".

Nel mondo Python, l'uso delle eccezioni per il controllo del flusso è comune e normale.

Perfino gli sviluppatori core di Python usano eccezioni per il controllo del flusso e quello stile è fortemente inserito nel linguaggio (cioè il protocollo iteratore usa StopIteration per terminare il loop del segnale).

Inoltre, lo stile try-tranne è usato per prevenire le condizioni di gara insite in alcuni dei costrutti "look-before-you-jump" . Ad esempio, test di os.path.exist produce informazioni che potrebbero non essere aggiornate al momento dell'uso. Allo stesso modo, Queue.full restituisce informazioni che potrebbero essere obsolete. Lo stile try-tranne-else produrrà codice più affidabile in questi casi.

"Comprendo che le eccezioni non sono errori, dovrebbero essere utilizzate solo per condizioni eccezionali"

In alcune altre lingue, quella regola riflette le loro norme culturali come si riflettono nelle loro biblioteche. La "regola" si basa anche in parte sulle considerazioni sulle prestazioni di tali lingue.

La norma culturale di Python è leggermente diversa. In molti casi, è necessario utilizzare le eccezioni per il flusso di controllo. Inoltre, l'uso delle eccezioni in Python non rallenta il codice circostante e il codice chiamante come in alcuni linguaggi compilati (ad es. CPython implementa già il codice per il controllo delle eccezioni in ogni fase, indipendentemente dal fatto che si utilizzino o meno eccezioni).

In altre parole, la tua comprensione che "le eccezioni sono eccezionali" è una regola che ha senso in alcune altre lingue, ma non per Python.

"Tuttavia, se è incluso nella stessa lingua, ci deve essere una buona ragione, vero?"

Oltre ad aiutare a evitare le condizioni di gara, le eccezioni sono anche molto utili per estrarre i cicli esterni di gestione degli errori. Questa è un'ottimizzazione necessaria nei linguaggi interpretati che non tendono ad avere un movimento di codice invariante ad anello automatico .

Inoltre, le eccezioni possono semplificare un po 'il codice in situazioni comuni in cui la capacità di gestire un problema è molto lontana da dove si è verificato il problema. Ad esempio, è comune disporre di un codice di livello superiore dell'interfaccia utente che chiama il codice per la logica aziendale che a sua volta chiama le routine di basso livello. Le situazioni che si presentano nelle routine di basso livello (come i record duplicati per chiavi univoche negli accessi al database) possono essere gestite solo nel codice di livello superiore (come chiedere all'utente una nuova chiave che non sia in conflitto con le chiavi esistenti). L'uso di eccezioni per questo tipo di flusso di controllo consente alle routine di medio livello di ignorare completamente il problema e di essere ben disaccoppiate da quell'aspetto del controllo di flusso.

C'è un bel post sul blog sull'indispensabilità delle eccezioni qui .

Inoltre, vedi questa risposta Stack Overflow: le eccezioni sono davvero per errori eccezionali?

"Qual è il motivo dell'esistenza di try-tranne-else?"

La clausola else stessa è interessante. Funziona quando non c'è eccezione, ma prima della clausola finally. Questo è il suo scopo principale.

Senza la clausola else, l'unica opzione per eseguire codice aggiuntivo prima della finalizzazione sarebbe la pratica goffa di aggiungere il codice alla clausola try. Questo è maldestro perché rischia di sollevare eccezioni nel codice che non era progettato per essere protetto dal try-block.

Il caso d'uso dell'esecuzione di codice aggiuntivo non protetto prima della finalizzazione non si presenta molto spesso. Quindi, non aspettarti di vedere molti esempi nel codice pubblicato. È piuttosto raro.

Un altro caso d'uso per la clausola else è quello di eseguire azioni che devono verificarsi quando non si verifica alcuna eccezione e che non si verificano quando vengono gestite le eccezioni. Per esempio:

recip = float('Inf')
try:
    recip = 1 / f(x)
except ZeroDivisionError:
    logging.info('Infinite result')
else:
    logging.info('Finite result')

Un altro esempio si verifica nei corridori unittest:

try:
    tests_run += 1
    run_testcase(case)
except Exception:
    tests_failed += 1
    logging.exception('Failing test case: %r', case)
    print('F', end='')
else:
    logging.info('Successful test case: %r', case)
    print('.', end='')

Infine, l'uso più comune di una clausola else in un blocco try è per un po 'di abbellimento (allineando i risultati eccezionali e quelli non eccezionali allo stesso livello di rientro). Questo uso è sempre facoltativo e non strettamente necessario.


28
"Questo è goffo perché rischia di sollevare eccezioni nel codice che non era progettato per essere protetto dal try-block." Questo è l'apprendimento più importante qui
Felix Dombek,

2
Grazie per la risposta. Per i lettori che cercano esempi di utilizzo di try-tranne-else, dai un'occhiata al metodo copyfile di shutil github.com/python/cpython/blob/master/Lib/shutil.py#L244
suripoori

2
Il punto è che la clausola else viene eseguita solo quando la clausola try ha esito positivo.
Jonathan,

173

Qual è il motivo per cui esiste try-tranne-else?

Un tryblocco consente di gestire un errore previsto. Il exceptblocco dovrebbe rilevare solo le eccezioni che sei pronto a gestire. Se gestisci un errore imprevisto, il tuo codice potrebbe fare la cosa sbagliata e nascondere i bug.

Una elseclausola verrà eseguita se non ci sono errori e, non eseguendo quel codice nel tryblocco, si evita di rilevare un errore imprevisto. Ancora una volta, la cattura di un errore imprevisto può nascondere i bug.

Esempio

Per esempio:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
else:
    return something

La suite "provare, tranne" ha due clausole opzionali, else e finally. Quindi in realtà lo è try-except-else-finally.

else valuterà solo se non vi è alcuna eccezione dal try blocco. Ci consente di semplificare il codice più complicato di seguito:

no_error = None
try:
    try_this(whatever)
    no_error = True
except SomeException as the_exception:
    handle(the_exception)
if no_error:
    return something

quindi se confrontiamo un else un'alternativa (che potrebbe creare bug) vediamo che riduce le righe di codice e possiamo avere una base di codice più leggibile, gestibile e meno buggy.

finally

finally verrà eseguito indipendentemente da ciò, anche se un'altra riga viene valutata con un'istruzione return.

Suddiviso con pseudo-codice

Potrebbe aiutare a scomporlo, nella forma più piccola possibile che dimostra tutte le funzionalità, con commenti. Supponiamo che questo pseudo-codice sia sintatticamente corretto (ma non eseguibile a meno che non vengano definiti i nomi) in una funzione.

Per esempio:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle_SomeException(the_exception)
    # Handle a instance of SomeException or a subclass of it.
except Exception as the_exception:
    generic_handle(the_exception)
    # Handle any other exception that inherits from Exception
    # - doesn't include GeneratorExit, KeyboardInterrupt, SystemExit
    # Avoid bare `except:`
else: # there was no exception whatsoever
    return something()
    # if no exception, the "something()" gets evaluated,
    # but the return will not be executed due to the return in the
    # finally block below.
finally:
    # this block will execute no matter what, even if no exception,
    # after "something" is eval'd but before that value is returned
    # but even if there is an exception.
    # a return here will hijack the return functionality. e.g.:
    return True # hijacks the return in the else clause above

E 'vero che abbiamo potuto includere il codice nel elseblocco nel tryblocco, invece, dove sarebbe eseguito se non ci fossero delle eccezioni, ma cosa succede se quel codice si solleva un'eccezione del genere stiamo cattura? Lasciandolo nel tryblocco nasconderebbe quel bug.

Vogliamo ridurre al minimo le righe di codice nel tryblocco per evitare di rilevare eccezioni che non ci aspettavamo, in base al principio che se il nostro codice fallisce, vogliamo che fallisca ad alta voce. Questa è una buona pratica .

Comprendo che le eccezioni non sono errori

In Python, la maggior parte delle eccezioni sono errori.

Possiamo visualizzare la gerarchia delle eccezioni usando pydoc. Ad esempio, in Python 2:

$ python -m pydoc exceptions

o Python 3:

$ python -m pydoc builtins

Ci darà la gerarchia. Possiamo vedere che la maggior parte dei tipi di Exceptionerrori sono, sebbene Python ne usi alcuni per cose come la fine di forloop ( StopIteration). Questa è la gerarchia di Python 3:

BaseException
    Exception
        ArithmeticError
            FloatingPointError
            OverflowError
            ZeroDivisionError
        AssertionError
        AttributeError
        BufferError
        EOFError
        ImportError
            ModuleNotFoundError
        LookupError
            IndexError
            KeyError
        MemoryError
        NameError
            UnboundLocalError
        OSError
            BlockingIOError
            ChildProcessError
            ConnectionError
                BrokenPipeError
                ConnectionAbortedError
                ConnectionRefusedError
                ConnectionResetError
            FileExistsError
            FileNotFoundError
            InterruptedError
            IsADirectoryError
            NotADirectoryError
            PermissionError
            ProcessLookupError
            TimeoutError
        ReferenceError
        RuntimeError
            NotImplementedError
            RecursionError
        StopAsyncIteration
        StopIteration
        SyntaxError
            IndentationError
                TabError
        SystemError
        TypeError
        ValueError
            UnicodeError
                UnicodeDecodeError
                UnicodeEncodeError
                UnicodeTranslateError
        Warning
            BytesWarning
            DeprecationWarning
            FutureWarning
            ImportWarning
            PendingDeprecationWarning
            ResourceWarning
            RuntimeWarning
            SyntaxWarning
            UnicodeWarning
            UserWarning
    GeneratorExit
    KeyboardInterrupt
    SystemExit

Un commentatore ha chiesto:

Supponi di avere un metodo che esegue il ping di un'API esterna e desideri gestire l'eccezione in una classe esterna al wrapper dell'API, restituisci semplicemente e dal metodo nella clausola salvo dove e è l'oggetto dell'eccezione?

No, non si restituisce l'eccezione, ma si rialza nuovamente con uno bare raiseper preservare lo stack.

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    raise

Oppure, in Python 3, puoi sollevare una nuova eccezione e preservare il backtrace con il concatenamento delle eccezioni:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    raise DifferentException from the_exception

Ho elaborato nella mia risposta qui .


upvoted! cosa fai di solito all'interno della parte della maniglia? supponiamo che tu abbia un metodo che esegue il ping di un'API esterna e desideri gestire l'eccezione in una classe esterna al wrapper dell'API, restituisci semplicemente e dal metodo nella clausola salvo dove e è l'oggetto dell'eccezione?
PirateApp

1
@PirateApp grazie! no, non restituirlo, probabilmente dovresti rilanciare con un raiseconcatenamento bare o fare eccezione - ma questo è più sull'argomento e trattato qui: stackoverflow.com/q/2052390/541136 - Probabilmente rimuoverò questi commenti dopo che visto che li hai visti.
Aaron Hall

grazie mille per i dettagli! passando per il post ora
PirateApp

36

Python non aderisce all'idea che le eccezioni debbano essere utilizzate solo per casi eccezionali, in realtà il linguaggio è "chiedi perdono, non permesso" . Ciò significa che l'utilizzo delle eccezioni come parte ordinaria del controllo del flusso è perfettamente accettabile e, di fatto, incoraggiato.

Questa è generalmente una buona cosa, poiché lavorare in questo modo aiuta ad evitare alcuni problemi (come esempio ovvio, le condizioni di gara sono spesso evitate) e tende a rendere il codice un po 'più leggibile.

Immagina di avere una situazione in cui prendi alcuni input dell'utente che devono essere elaborati, ma hai un valore predefinito che è già stato elaborato. La try: ... except: ... else: ...struttura rende il codice molto leggibile:

try:
   raw_value = int(input())
except ValueError:
   value = some_processed_value
else: # no error occured
   value = process_value(raw_value)

Confronta con come potrebbe funzionare in altre lingue:

raw_value = input()
if valid_number(raw_value):
    value = process_value(int(raw_value))
else:
    value = some_processed_value

Nota i vantaggi. Non è necessario verificare che il valore sia valido e analizzarlo separatamente, vengono eseguite una volta. Il codice segue anche una progressione più logica, il percorso del codice principale è il primo, seguito da "se non funziona, fallo".

L'esempio è naturalmente un po 'inventato, ma mostra che ci sono casi per questa struttura.


15

È buona norma usare try-tranne-else in Python?

La risposta è che dipende dal contesto. Se lo fai:

d = dict()
try:
    item = d['item']
except KeyError:
    item = 'default'

Dimostra che non conosci molto bene Python. Questa funzionalità è incapsulata nel dict.getmetodo:

item = d.get('item', 'default')

Il try/except blocco è un modo molto più visivamente ingombrante e dettagliato di scrivere ciò che può essere eseguito in modo efficiente in una sola riga con un metodo atomico. Ci sono altri casi in cui questo è vero.

Tuttavia, ciò non significa che dovremmo evitare tutta la gestione delle eccezioni. In alcuni casi si preferisce evitare le condizioni di gara. Non controllare se esiste un file, prova solo ad aprirlo e cattura l'IOError appropriato. Per motivi di semplicità e leggibilità, prova a incapsulare questo o a considerarlo come a proposito.

Leggi lo Zen di Python , comprendendo che ci sono principi in tensione e diffida dal dogma che si basa troppo pesantemente su una qualsiasi delle affermazioni in esso contenute.


12

Vedi il seguente esempio che illustra tutto su try-tranne-else-finally:

for i in range(3):
    try:
        y = 1 / i
    except ZeroDivisionError:
        print(f"\ti = {i}")
        print("\tError report: ZeroDivisionError")
    else:
        print(f"\ti = {i}")
        print(f"\tNo error report and y equals {y}")
    finally:
        print("Try block is run.")

Implementalo e vieni:

    i = 0
    Error report: ZeroDivisionError
Try block is run.
    i = 1
    No error report and y equals 1.0
Try block is run.
    i = 2
    No error report and y equals 0.5
Try block is run.

3
Questo è un grande, semplice esempio che dimostra rapidamente la clausola try completa senza richiedere a qualcuno (che potrebbe avere una certa fretta) di leggere una lunga spiegazione astratta. (Ovviamente, quando non hanno più fretta, dovrebbero tornare e leggere l'intero abstract.)
GlobalSoftwareSociety

6

Dovresti stare attento all'utilizzo del blocco finally, poiché non è la stessa cosa che usare un blocco else nel tentativo, tranne. Il blocco finally verrà eseguito indipendentemente dal risultato del tentativo tranne.

In [10]: dict_ = {"a": 1}

In [11]: try:
   ....:     dict_["b"]
   ....: except KeyError:
   ....:     pass
   ....: finally:
   ....:     print "something"
   ....:     
something

Come tutti hanno notato usando il blocco else, il codice diventa più leggibile e viene eseguito solo quando non viene generata un'eccezione

In [14]: try:
             dict_["b"]
         except KeyError:
             pass
         else:
             print "something"
   ....:

So che finalmente viene sempre eseguito, ed è per questo che può essere usato a nostro vantaggio impostando sempre un valore predefinito, quindi in caso di eccezione viene restituito, se non vogliamo restituire tale valore in caso di eccezione, esso è sufficiente per rimuovere il blocco finale. A proposito, usare pass su una cattura di eccezione è qualcosa che non avrei mai fatto :)
Juan Antonio Gomez Moriano

@Juan Antonio Gomez Moriano, il mio blocco di codifica è solo a scopo di esempio. Nemmeno io avrei mai usato il pass
Greg

4

Ogni volta che vedi questo:

try:
    y = 1 / x
except ZeroDivisionError:
    pass
else:
    return y

O anche questo:

try:
    return 1 / x
except ZeroDivisionError:
    return None

Considera questo invece:

import contextlib
with contextlib.suppress(ZeroDivisionError):
    return 1 / x

1
Non risponde alla mia domanda, poiché quello era semplicemente un esempio amico mio.
Juan Antonio Gomez Moriano,

In Python, le eccezioni non sono errori. Non sono nemmeno eccezionali. In Python, è normale e naturale utilizzare le eccezioni per il controllo del flusso. Ciò è evidenziato dall'inclusione di contextlib.suppress () nella libreria standard. Vedi la risposta di Raymond Hettinger qui: stackoverflow.com/a/16138864/1197429 (Raymond è un collaboratore principale di Python e un'autorità su tutte le cose Pythonic!)
Rajiv Bakulesh Shah,

4

Solo perché nessun altro ha pubblicato questa opinione, direi

evitare le elseclausole try/excepts perché non sono familiari con la maggior parte delle persone

A differenza delle parole chiave try, excepte finally, il significato della elseclausola non è evidente; è meno leggibile. Poiché non viene usato molto spesso, le persone che leggono il tuo codice vorranno ricontrollare i documenti per essere sicuri di capire cosa sta succedendo.

(Sto scrivendo questa risposta proprio perché ho trovato un try/except/elsenel mio codebase e ha causato un momento wtf e mi ha costretto a fare un po 'di googling).

Quindi, ovunque vedo codice come nell'esempio OP:

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
else:
    # do some more processing in non-exception case
    return something

Preferirei fare il refactoring

try:
    try_this(whatever)
except SomeException as the_exception:
    handle(the_exception)
    return  # <1>
# do some more processing in non-exception case  <2>
return something
  • <1> ritorno esplicito, mostra chiaramente che, in caso di eccezione, abbiamo finito di lavorare

  • <2> come piacevole effetto secondario, il codice che elsesi trovava nel blocco è dedotto di un livello.


1
Argomentazione del diavolo contro-argomento: più persone lo usano, più sarà adottato. Solo spunti di riflessione, anche se concordo sul fatto che la leggibilità è importante. Detto questo, una volta che qualcuno capisce try-else, direi che è molto più leggibile dell'alternativa in molti casi.
bob

2

Questo è il mio semplice frammento su come capire il blocco try-tranne-else-finally in Python:

def div(a, b):
    try:
        a/b
    except ZeroDivisionError:
        print("Zero Division Error detected")
    else:
        print("No Zero Division Error")
    finally:
        print("Finally the division of %d/%d is done" % (a, b))

Proviamo div 1/1:

div(1, 1)
No Zero Division Error
Finally the division of 1/1 is done

Proviamo div 1/0

div(1, 0)
Zero Division Error detected
Finally the division of 1/0 is done

1
Penso che questo non sia un esempio del perché non puoi semplicemente inserire il codice else nel tentativo
Mojimi,

-4

OP, SEI CORRETTO. L'altro dopo aver provato / tranne in Python è brutto . conduce a un altro oggetto di controllo del flusso dove non è necessario nessuno:

try:
    x = blah()
except:
    print "failed at blah()"
else:
    print "just succeeded with blah"

Un equivalente totalmente chiaro è:

try:
    x = blah()
    print "just succeeded with blah"
except:
    print "failed at blah()"

Questo è molto più chiaro di un'altra clausola. L'altro dopo aver provato / salvo non è scritto frequentemente, quindi ci vuole un momento per capire quali sono le implicazioni.

Solo perché PUOI fare una cosa, non significa che DOVREBBE fare una cosa.

Molte funzionalità sono state aggiunte alle lingue perché qualcuno ha pensato che potesse tornare utile. Il problema è che più sono le caratteristiche, meno le cose sono chiare e ovvie perché le persone di solito non usano quelle campane e fischietti.

Solo i miei 5 centesimi qui. Devo venire dietro e ripulire un sacco di codice scritto da sviluppatori universitari del 1 ° anno che pensano di essere intelligenti e vogliono scrivere codice in un modo super-stretto ed estremamente efficiente quando questo lo rende solo un casino per provare a leggere / modificare in seguito. Voto per la leggibilità ogni giorno e due volte la domenica.


15
Hai ragione. Questo è totalmente chiaro ed equivalente ... a meno che non sia la tua printaffermazione che fallisce. Cosa succede se x = blah()restituisce a str, ma la tua dichiarazione di stampa è print 'just succeeded with blah. x == %d' % x? Ora hai un TypeErroressere generato dove non sei pronto a gestirne uno; stai ispezionando x = blah()per trovare la fonte dell'eccezione e non è nemmeno lì. L'ho fatto (o l'equivalente) più di una volta in cui elsemi avrebbero impedito di fare questo errore. Adesso lo so meglio. MrGreen
Doug R.

2
... e sì, hai ragione. La elseclausola non è un'affermazione carina e fino a quando non ci si abitua, non è intuitiva. Ma poi, nessuno dei due è stato finallyquando ho iniziato a usarlo ...
Doug R.

2
Per fare eco a Doug R., non è equivalente perché le eccezioni durante le dichiarazioni nella elseclausola non sono colte dal except.
alastair,

se ... tranne ... else è più leggibile, altrimenti devi leggere "oh, dopo il blocco try, e nessuna eccezione, vai all'istruzione al di fuori del blocco try", quindi usando else: tende a collegare un po 'semanticamente la sintassi meglio imo. Inoltre, è buona norma lasciare le istruzioni non intrappolate al di fuori del blocco di prova iniziale.
cowbert,

1
@DougR. "stai ispezionando x = blah()per trovare la fonte dell'eccezione", avendo tracebackperché dovresti ispezionare la fonte dell'eccezione da un posto sbagliato?
Nehem,
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.