2009-06-12 17 views
26

He buscado otras publicaciones, ya que considero que este es un problema bastante común, pero todas las demás preguntas de excepción de Python que he encontrado no reflejan mi problema.¿Manera correcta de manejar excepciones en Python?

Trataré de ser tan específico aquí como pueda, así que daré un ejemplo directo. Y por favor, no publique ninguna solución para este problema específico. No estoy interesado específicamente en cómo puede enviar un correo electrónico mucho mejor con xyz. Quiero saber cómo generalmente se ocupa de las declaraciones dependientes propensas a errores.

Mi pregunta es cómo manejar bien las excepciones, unas que dependen unas de otras, es decir: Solo si el primer paso fue exitoso, intente con el siguiente, y así sucesivamente. Un criterio más es: todas las excepciones deben ser atrapadas, este código tiene que ser robusto.

Para su consideración, un ejemplo:

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 

Esto se ve muy unpythonic a mí, y el código de control de errores es el triple del código de negocio real, pero, por otra parte, ¿cómo puedo manejar varias declaraciones que son Depende de los otros, lo que significa que la declaración1 es un requisito previo para la declaración2 y así sucesivamente.

También estoy interesado en la limpieza adecuada de los recursos, incluso Python puede hacerlo por sí mismo.

Gracias, Tom

+1

gracias por editar dbr, pero por favor no edite cosas de las que no esté seguro. Modifiqué los criterios hacia el criterio, que de hecho es el singular, ya que un plural no tiene sentido donde lo editó. – Tom

+0

Opps, perdón por eso (hm, no creo que haya escuchado el singular criterio antes ...) – dbr

Respuesta

24

En lugar de utilizar el try/except es el bloque más, usted podría simplemente regresa cuando él los errores:

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() 

Eso es perfectamente legible y Pythonic ..

Otra forma de hacer esto es, en lugar de preocuparse acerca de la aplicación específica, decidir cómo quiere que su código se vea, por ejemplo ..

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!" 

luego escribir el código para el método message(), ca Instalar cualquier error que espere, y elevar el suyo propio, y manejarlo cuando sea relevante. Su clase puede parecer algo así ..

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 Me gusta mucho cómo ha resuelto el problema "la biblioteca solo usa una excepción para todo" –

+1

En su primer ejemplo, send_message() siempre volverá después de server.login() y nunca enviará el mensaje. No creo que haya un finalmente para esta declaración. – mhawke

+1

, ahora se reduce a una cuestión de principio.su primer código es básicamente el mismo que el mío, excepto que simplemente no anida las excepciones, como hice en un árbol "else", que se sugirió en los documentos de python. ¿Cuál es mejor práctica? los documentos indican que siempre se debe preferir más en lugar de declaraciones adicionales en el bloque try. es básicamente la misma pregunta con if: ¿es mejor regresar antes que otro si, o es mejor anidar el ifs condicionalmente? – Tom

0

¿Por qué no un gran intento: bloque? De esta forma, si se detecta alguna excepción, irá hasta el extremo excepto. Y siempre que todas las excepciones para los diferentes pasos sean diferentes, siempre puede decir qué parte fue la que activó la excepción.

+0

porque un bloque de prueba grande no te dará la oportunidad de cuidar los recursos. Por ejemplo: statement1 funcionó, y abrió una archivo, pero el enunciado 2 arroja una excepción. No puede manejar eso en un solo bloque final, porque nunca se sabe si el recurso fue realmente asignado o no. Además, algunos pasos pueden disparar las mismas excepciones, por lo que no se puede determinar qué salió mal más adelante, y no puede imprimir errores, porque no está seguro de qué afirmación realmente falló. – Tom

+0

Anidados con bloques, entonces? http://docs.python.org/whatsnew/2.5.html#pe p-343 –

+0

lamentablemente no puedo confiar en los métodos __enter__ y __exit__ que se definirán para todas las operaciones que usaré, por lo que con might siempre no funcionarán – Tom

12

En general, desea utilizar la menor cantidad posible de bloques de prueba, distinguiendo las condiciones de falla por los tipos de excepciones que arrojan. Por ejemplo, aquí está mi refactorización del código que envió:

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 

Aquí, utilizamos el hecho de que smtplib.SMTP(), server.login(), y server.sendmail() todos tiran diferentes excepciones para aplanar el árbol de los bloques try-catch. En el bloque finally, probamos el servidor explícitamente para evitar invocar a quit() en el objeto nil.

También pudimos usar tres bloques try-catch secuenciales, volviendo False en las condiciones de excepción, si hay superposición de los casos de excepción que necesitan ser manejadas por separado:

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 

Esto no es tan agradable, ya que tiene que matar el servidor en más de un lugar, pero ahora podemos manejar tipos de excepciones específicas de diferentes maneras en diferentes lugares sin mantener ningún estado adicional.

+5

el punto es, como se indicó anteriormente, que no arrojan excepciones individuales, por lo que no se puede aplanar trivialmente. En caso de que su conexión se interrumpa antes de que pueda realizar la autenticación, server.login y server.sendMail podrían lanzar la misma excepción ("connect to server first") Pero como también dije anteriormente, no estoy buscando una solución a este problema específico problema. Estoy más interesado en un enfoque general sobre cómo resolver esto. Su segundo enfoque es básicamente mi código sin "else". es más bonito, aunque tengo que admitirlo;) – Tom

+1

Tenga cuidado con el bloque finally: querrá configurar el servidor en Ninguno antes del bloque para evitar referencias accidentales a una variable inexistente. –

+0

@Tom +1, es por eso que no sugerí esta solución. – Unknown

0

Me gusta la respuesta de David, pero si está atascado en las excepciones del servidor también puede verificar si el servidor es Ninguno o estados. Aplané el método un poco, sigue siendo un aspecto poco psicótico pero más legible en la lógica de abajo.

server = None 

def server_obtained(host): 
    try: 
     server = smtplib.SMTP(host) #can throw an exception 
     return True 
    except smtplib.socket.gaierror: 
     #actually it can throw a lot more, this is just an example 
     return False 

def server_login(username, password): 
    loggedin = False 
    try: 
     server.login(username, password) 
     loggedin = True 
    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! 
     if(server is not None): 
      server.quit() 
    return loggedin 

def send_mail(addr, to, msg): 
    sent = False 
    try: 
     server.sendmail(addr, to, msg) 
     sent = True 
    except Exception: 
     return False 
    finally: 
     server.quit() 
    return sent 

def do_msg_send(): 
    if(server_obtained(host)): 
     if(server_login(username, password)): 
      if(send_mail(addr, [to], msg.as_string())): 
       return True 
    return False 
+0

tanto en server_login como en send_mail, puede evitar la variable local, porque finalmente siempre se ejecutará, incluso si usa un "retorno" en un intento o excepto en el bloque :) simplemente puede devolver True en el bloque try y False en el bloquear en lugar de guardar el estado en una var. – Tom

1

Simplemente usando un try-block es el camino a seguir. Esto es exactamente lo que ellos están diseñados para: solo ejecutar la siguiente declaración si la declaración anterior no lanzó una excepción. En cuanto a las limpiezas de recursos, quizás pueda verificar el recurso si necesita ser limpiado (por ejemplo, miarchivo.is_open(), ...) Esto agrega algunas condiciones adicionales, pero solo se ejecutarán en el caso excepcional. Para manejar el caso de que la misma excepción se puede originar por diferentes razones, usted debería poder recuperar el motivo de la excepción.

que sugieren código como este:

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 

Se ninguna raro, que el código de control de errores excede código de negocio. Corregir el manejo de errores puede ser complejo. Pero para aumentar el mantenimiento, es útil separar el código comercial del código de manejo de errores.

+0

no estoy de acuerdo, porque como he dicho bastantes veces arriba, los mensajes de error serían ambiguos, ya que 2 llamadas a función diferentes, por ejemplo, inicio de sesión y sendmail, arrojarían la misma excepción. Si desea imprimir al usuario o a su registro: "login() failed debido a xyz" o "sendmail() failed debido a xyz" esto no es posible, ya que ambas llamadas pueden dar como resultado la misma excepción. Quiero un manejo detallado de lo que salió mal para fines de registro. – Tom

+0

La excepción debería ser capaz de proporcionar sus detalles. P.ej. puede usar "excepto gaierror, (código, mensaje):", en lugar de simple "excepto gaierror:". Luego tiene el código de error y el mensaje de error y puede usarlos para un manejo de errores detallado, p. si el código == 11001: imprime "nombre de host desconocido:", mensaje – Ralph

+0

Creo que no has entendido por completo lo que estaba tratando de decir. Pruebe lo siguiente: hágase un objeto SMTP, y luego pruebe smtp.login() sin conectarse, y luego smtp.sendmail() sin conectarse, verá que arrojan excepciones 100% idénticas, que no puede distinguir, ni por msg ni por errno – Tom

3

Si era yo que probablemente hacer algo como lo siguiente:

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 

Todos los errores son capturados y depuradas, el valor de retorno == true en caso de éxito , y la conexión del servidor se limpia correctamente si se realiza la conexión inicial.

+0

esto me parece intuitivo, y probablemente también sea lo que se vería en Java, porque no tiene el lujo de una declaración de "else" allí. Tampoco tienes esto antes de python2.5 iirc. Se introdujo en los documentos para evitar grandes bloques de prueba, pero aún así mantener la agrupación agradable y obvia, por lo que todos los códigos que pertenecen juntos seguirán en el mismo intento ... excepto ... else bloque, sin explotar la parte de prueba de Proprtions. como los pitonios están tan apegados a sus guías de estilo y peps, pensé que este sería el camino correcto a seguir. – Tom

1

me gustaría probar algo como esto:

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__) 

Todos los métodos (incluyendo send_message, en realidad) siguen el mismo protocolo: se devuelven True si tenían éxito, y, a menos que realmente mango una excepción, ellos no lo atrapan Este protocolo también permite manejar el caso donde un método necesita indicar que falló sin generar una excepción. (Si la única forma en que sus métodos fallan es mediante una excepción, eso simplifica el protocolo. Si tiene que lidiar con muchos estados de falla sin excepción fuera del método que fallaron, es probable que tenga un problema de diseño que aún no han funcionado.)

La desventaja de este enfoque es que todos los métodos tienen que usar los mismos argumentos. No he optado por ninguno, con la expectativa de que los métodos que he superado terminarán manipulando a los miembros de la clase.

Sin embargo, la ventaja de este enfoque es considerable. Primero, puede agregar docenas de métodos al proceso sin que send_message se vuelva más complejo.

También puede volverse loco y hacer algo como esto:

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." 

... aunque en ese momento, podría decir a mí mismo: "sí, que está trabajando el patrón Comando bastante difícil sin crear una clase de comando. Tal vez ahora es el momento."

Cuestiones relacionadas