2009-06-12 3 views
26

Ho cercato altri post, in quanto ritenevo che fosse un problema piuttosto comune, ma tutte le altre domande di eccezione di Python che ho trovato non riflettevano il mio problema.Modo corretto di gestire le eccezioni in Python?

Cercherò di essere il più specifico possibile, quindi darò un esempio diretto. E per favore non pubblicare alcuna soluzione alternativa per questo problema specifico. Non sono particolarmente interessato a come è possibile inviare un'e-mail molto più bella con xyz. Voglio sapere in che modo generalmente gestisci le istruzioni dipendenti e soggette a errori.

La mia domanda è, come gestire bene le eccezioni, quelle che dipendono l'una dall'altra, ovvero: Solo se il primo passaggio ha avuto esito positivo, provare il successivo e così via. Un altro criterio è: tutte le eccezioni devono essere catturate, questo codice deve essere robusto.

Per la vostra considerazione, un esempio:

try: 
    server = smtplib.SMTP(host) #can throw an exception 
except smtplib.socket.gaierror: 
    #actually it can throw a lot more, this is just an example 
    pass 
else: #only if no exception was thrown we may continue 
    try: 
     server.login(username, password) 
    except SMTPAuthenticationError: 
     pass # do some stuff here 
    finally: 
     #we can only run this when the first try...except was successful 
     #else this throws an exception itself! 
     server.quit() 
    else: 
     try: 
      # this is already the 3rd nested try...except 
      # for such a simple procedure! horrible 
      server.sendmail(addr, [to], msg.as_string()) 
      return True 
     except Exception: 
      return False 
     finally: 
      server.quit() 

return False 

Questo sembra estremamente unpythonic a me, e la gestione di codice di errore è il triplo il codice vero e proprio business, ma d'altra parte come posso gestire diverse affermazioni dipendenti l'uno dall'altro, il significato di statement1 è un prerequisito per statement2 e così via?

Sono interessato anche alla corretta pulizia delle risorse, anche Python può gestirlo da solo.

Grazie, Tom

+1

grazie per la modifica di dbr, ma per favore non modificare le cose che non sono sicuro di te. Ho modificato i criteri in base al criterio, che è effettivamente il singolare, in quanto il plurale non ha senso dove lo hai modificato – Tom

+0

Opps, mi dispiace (non credo di aver mai sentito prima il singolare dei criteri ..) – dbr

risposta

24

Invece di usare il try/tranne è blocco else, si potrebbe semplicemente tornare quando esso errori:

def send_message(addr, to, msg): 
    ## Connect to host 
    try: 
     server = smtplib.SMTP(host) #can throw an exception 
    except smtplib.socket.gaierror: 
     return False 

    ## Login 
    try: 
     server.login(username, password) 
    except SMTPAuthenticationError: 
     server.quit() 
     return False 

    ## Send message 
    try: 
     server.sendmail(addr, [to], msg.as_string()) 
     return True 
    except Exception: # try to avoid catching Exception unless you have too 
     return False 
    finally: 
     server.quit() 

Questo è perfettamente leggibile e Pythonic ..

Un altro modo per farlo è, piuttosto che preoccuparsi circa l'attuazione specifica, decidere come si desidera il codice a guardare, per esempio ..

sender = MyMailer("username", "password") # the except SocketError/AuthError could go here 
try: 
    sender.message("addr..", ["to.."], "message...") 
except SocketError: 
    print "Couldn't connect to server" 
except AuthError: 
    print "Invalid username and/or password!" 
else: 
    print "Message sent!" 

quindi scrivere il codice per il metodo di message(), ca prendere in mano gli errori che ci si aspetta, e aumentarne uno personalizzato e gestirli laddove è pertinente. La classe può sembrare qualcosa di simile ..

class ConnectionError(Exception): pass 
class AuthError(Exception): pass 
class SendError(Exception): pass 

class MyMailer: 
    def __init__(self, host, username, password): 
     self.host = host 
     self.username = username 
     self.password = password 

    def connect(self): 
     try: 
      self.server = smtp.SMTP(self.host) 
     except smtplib.socket.gaierror: 
      raise ConnectionError("Error connecting to %s" % (self.host)) 

    def auth(self): 
     try: 
      self.server.login(self.username, self.password) 
     except SMTPAuthenticationError: 
      raise AuthError("Invalid username (%s) and/or password" % (self.username)) 

    def message(self, addr, to, msg): 
     try: 
      server.sendmail(addr, [to], msg.as_string()) 
     except smtplib.something.senderror, errormsg: 
      raise SendError("Couldn't send message: %s" % (errormsg)) 
     except smtp.socket.timeout: 
      raise ConnectionError("Socket error while sending message") 
+4

+1 Mi piace molto il modo in cui hai risolto il problema "la libreria utilizza solo un'eccezione per tutto" –

+1

Nel tuo primo esempio, send_message() tornerà sempre dopo il server.login() e non invierà mai il messaggio. Non penso che ci dovrebbe essere un fine per questa affermazione. – mhawke

+1

quindi si riduce a una questione di principio.il tuo primo codice è fondamentalmente uguale al mio, tranne per il semplice fatto che non si annidano le eccezioni, come ho fatto in un "altro" -tree, che è stato suggerito nei documenti python. qual è la migliore pratica? i documenti affermano che altrimenti dovrebbe essere sempre preferito piuttosto che dichiarazioni aggiuntive nel blocco try. è fondamentalmente la stessa domanda con if: è meglio tornare prima di un altro se, o è meglio annidare i if in modo condizionale. – Tom

0

Perché non una grande prova: block? In questo modo, se viene rilevata un'eccezione, vai fino all'eccezione. E fintanto che tutte le eccezioni per i diversi passaggi sono diverse, puoi sempre dire quale parte ha generato l'eccezione.

+0

perché un grosso blocco di prova non ti dà la possibilità di occuparti delle risorse.Esso: statement1 ha funzionato e ha aperto un file, ma l'istruzione 2 genera un'eccezione. Non puoi gestirla in un unico blocco finally, perché non sai mai se la risorsa è stata effettivamente allocata o meno. Inoltre, alcuni passaggi possono attivare le stesse eccezioni, quindi non è possibile determinare cosa è andato storto in seguito, e non puoi stampare errori, perché non sei sicuro di quale affermazione sia effettivamente fallita: – Tom

+0

nidificato con blocchi, quindi? http://docs.python.org/whatsnew/2.5.html#pe p-343 –

+0

purtroppo non posso fare affidamento sui metodi __enter__ e __exit__ da definire per tutte le operazioni che userò, quindi con potrebbe sempre non funzionare – Tom

12

In generale, si desidera utilizzare il minor numero possibile di blocchi di prova, distinguendo le condizioni di errore dai tipi di eccezioni che generano. Per esempio, ecco la mia refactoring del codice che hai postato:

try: 
    server = smtplib.SMTP(host) 
    server.login(username, password) # Only runs if the previous line didn't throw 
    server.sendmail(addr, [to], msg.as_string()) 
    return True 
except smtplib.socket.gaierror: 
    pass # Couldn't contact the host 
except SMTPAuthenticationError: 
    pass # Login failed 
except SomeSendMailError: 
    pass # Couldn't send mail 
finally: 
    if server: 
     server.quit() 
return False 

Qui, usiamo il fatto che smtplib.SMTP(), server.login(), e server.sendmail() tutto buttare diverse eccezioni per appiattire l'albero dei blocchi try-catch. Nel blocco finally testiamo esplicitamente il server per evitare di invocare quit() sull'oggetto nil.

Potremmo anche utilizzare tre sequenziali blocchi try-catch, tornando False nelle condizioni di eccezione, se ci sono sovrapposti i casi di eccezione che hanno bisogno di essere trattata separatamente:

try: 
    server = smtplib.SMTP(host) 
except smtplib.socket.gaierror: 
    return False # Couldn't contact the host 

try: 
    server.login(username, password) 
except SMTPAuthenticationError: 
    server.quit() 
    return False # Login failed 

try: 
    server.sendmail(addr, [to], msg.as_string()) 
except SomeSendMailError: 
    server.quit() 
    return False # Couldn't send mail 

return True 

Questo non è così bello, dato che devi uccidere il server in più di un posto, ma ora possiamo gestire tipi di eccezioni specifiche in modi diversi in luoghi diversi senza mantenere nessuno stato aggiuntivo.

+5

il punto è, come detto sopra, che non generano eccezioni individuali, quindi non può essere banalmente appiattito. Nel caso in cui la tua connessione venga interrotta prima che tu possa autorizzare, server.login e server.sendMail potrebbero lanciare la stessa eccezione ("connetti prima al server") Ma come ho già detto sopra, non sto cercando una soluzione a questo specifico problema. Sono più interessato a un approccio generale su come risolvere questo problema. Il tuo secondo approccio è fondamentalmente il mio codice senza "altro". è più carino anche se devo ammettere;) – Tom

+1

Attenzione sul blocco finally - ti consigliamo di impostare il server su Nessuno prima del blocco per evitare di fare riferimento accidentalmente a una variabile inesistente. –

+0

@Tom +1, ecco perché non ho suggerito questa soluzione. – Unknown

0

Mi piace la risposta di David, ma se sei bloccato sulle eccezioni del server puoi anche controllare se il server è None o states. Ho appiattito un po 'il metodo, ma è ancora un po' disordinato ma più leggibile nella logica in basso.

+0

in server_login e send_mail puoi evitare la variabile locale, perché alla fine verrà sempre eseguito, anche se utilizzi un "return" in un try o ad eccezione di un blocco :) puoi semplicemente restituire True nel blocco try e False nell'eccezione blocco invece di salvare lo stato in una var locale. – Tom

1

Utilizzare solo un try-block è la strada da percorrere. Questo è esattamente ciò che sono progettati per: : eseguire la dichiarazione successiva solo se la precedente dichiarazione non ha generato un'eccezione. Per quanto riguarda i clean-up delle risorse, forse è possibile controllare la risorsa se deve essere ripulita (ad esempio myfile.is_open(), ...) Questo aggiunge alcune condizioni extra, ma verranno eseguite solo in casi eccezionali. Per gestire il caso che la stessa eccezione può essere sollevata per diversi motivi, è possibile che sia in grado di recuperare il motivo dall'eccezione.

suggerisco codice come questo:

server = None 
try: 
    server = smtplib.SMTP(host) #can throw an exception 
    server.login(username, password) 
    server.sendmail(addr, [to], msg.as_string()) 
    server.quit() 
    return True 
except smtplib.socket.gaierror: 
    pass # do some stuff here 
except SMTPAuthenticationError: 
    pass # do some stuff here 
except Exception, msg: 
    # Exception can have several reasons 
    if msg=='xxx': 
     pass # do some stuff here 
    elif: 
     pass # do some other stuff here 

if server: 
    server.quit() 

return False 

E non è raro, che gestisce il codice errore supera il codice di business. La corretta gestione degli errori può essere complessa. Ma per aumentare la manutenibilità aiuta a separare il codice aziendale dal codice di gestione degli errori.

+0

Non sono d'accordo, perché come ho affermato diverse volte sopra, i messaggi di errore sarebbero ambigui, poiché 2 diverse chiamate di funzione, ad esempio login e sendmail, potrebbero generare la stessa eccezione. Se si desidera stampare all'utente o al registro: "login() non riuscito a causa di xyz" o "sendmail() non riuscito a causa di xyz" questo non è possibile, poiché entrambe le chiamate potrebbero comportare la stessa eccezione. Voglio una gestione dettagliata di ciò che è andato storto per scopi di registrazione. – Tom

+0

L'eccezione dovrebbe essere in grado di fornire i suoi dettagli. Per esempio. puoi usare "tranne gaierror, (codice, messaggio):", invece di semplice "tranne gaierror:". Quindi hai il codice di errore e il messaggio di errore e puoi usarli per la gestione dettagliata degli errori, ad es. se codice == 11001: stampa "nome host sconosciuto:", messaggio – Ralph

+0

Penso che tu non abbia ottenuto pienamente ciò che stavo cercando di dire. Prova quanto segue: crea un oggetto SMTP, quindi prova a smtp.login() senza connettersi e quindi a smtp.sendmail() senza connettersi, vedrai che generano eccezioni identiche al 100%, che non puoi distinguere, né da msg né da da errno – Tom

3

Se mi fosse avrei probabilmente fare qualcosa di simile alla seguente:

try: 
    server = smtplib.SMTP(host) 
    try: 
     server.login(username, password) 
     server.sendmail(addr, [to], str(msg)) 
    finally: 
     server.quit() 
except: 
    debug("sendmail", traceback.format_exc().splitlines()[-1]) 
    return True 

Tutti gli errori vengono catturati e debug, il valore di ritorno == true in caso di successo e la connessione al server viene adeguatamente ripulita in caso di connessione iniziale.

+0

questo mi sembra intuitivo, e probabilmente è anche quello che sembrerebbe in Java, perché non hai il lusso di una "altra" affermazione lì. Anche tu non l'hai fatto prima di python2.5 iirc. È stato introdotto nei documenti per evitare blocchi di prova di grandi dimensioni, ma mantenere il raggruppamento piacevole ed evidente, quindi tutto il codice che appartiene insieme sarà ancora nella stessa prova ... eccetto ... altro blocco, senza far saltare la parte di prova da proprtions. come i pitoni sono così attaccati alle loro guide e ai loro peps di stile, ho pensato che questo sarebbe stato il modo corretto di andare. – Tom

1

vorrei provare qualcosa di simile:

class Mailer(): 

    def send_message(self): 
     exception = None 
     for method in [self.connect, 
         self.authenticate, 
         self.send, 
         self.quit]: 
      try: 
       if not method(): break 
      except Exception, ex: 
       exception = ex 
       break 

     if method == quit and exception == None: 
      return True 

     if exception: 
      self.handle_exception(method, exception) 
     else: 
      self.handle_failure(method) 

    def connect(self): 
     return True 

    def authenticate(self): 
     return True 

    def send(self): 
     return True 

    def quit(self): 
     return True 

    def handle_exception(self, method, exception): 
     print "{name} ({msg}) in {method}.".format(
      name=exception.__class__.__name__, 
      msg=exception, 
      method=method.__name__) 

    def handle_failure(self, method): 
     print "Failure in {0}.".format(method.__name__) 

Tutti i metodi (tra cui send_message, in realtà) seguono lo stesso protocollo: tornano True se ci sono riusciti, e, a meno che in realtà maniglia un'eccezione, non lo intrappolano. Questo protocollo rende anche possibile gestire il caso in cui un metodo deve indicare che non ha funzionato senza generare un'eccezione. (Se l'unico modo in cui i metodi falliscono è sollevando un'eccezione, che semplifica il protocollo.Se si ha a che fare con un sacco di stati di errore non eccezione al di fuori del metodo che ha fallito, probabilmente si ha un problema di progettazione non ha ancora funzionato.)

Lo svantaggio di questo approccio è che tutti i metodi devono utilizzare gli stessi argomenti. Non ho optato per nessuno, con l'aspettativa che i metodi che ho eliminato finiranno per manipolare i membri della classe.

L'aspetto positivo di questo approccio è tuttavia considerevole. Innanzitutto, è possibile aggiungere dozzine di metodi al processo senza rendere più complesso lo send_message.

Si può anche impazzire e fare qualcosa del genere:

def handle_exception(self, method, exception): 
    custom_handler_name = "handle_{0}_in_{1}".format(\ 
              exception.__class__.__name__, 
              method.__name__) 
    try: 
     custom_handler = self.__dict__[custom_handler_name] 
    except KeyError: 
     print "{name} ({msg}) in {method}.".format(
      name=exception.__class__.__name__, 
      msg=exception, 
      method=method.__name__) 
     return 
    custom_handler() 

def handle_AuthenticationError_in_authenticate(self): 
    print "Your login credentials are questionable." 

... anche se a quel punto, avrei potuto dire a me stesso, "auto, si sta lavorando il pattern Command piuttosto difficile, senza creare una classe di comando. Forse ora è il momento."