代码之家  ›  专栏  ›  技术社区  ›  Tom

在python中处理异常的正确方法?

  •  27
  • Tom  · 技术社区  · 17 年前

    我搜索了其他文章,因为我觉得这是一个相当常见的问题,但是我发现的所有其他Python异常问题都没有反映我的问题。

    我会尽量在这里具体,所以我会给出一个直接的例子。请不要为这个特定的问题发布任何解决方法。我不特别感兴趣,你如何用XYZ发送一封更好的电子邮件。 我想知道您通常如何处理依赖的、容易出错的语句。

    我的问题是,如何很好地处理异常,即相互依赖的异常,这意味着: 只有当第一步成功时,才尝试下一步,依此类推。另一个标准是:必须捕获所有异常,这段代码必须是健壮的。

    供您考虑,举一个例子:

    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
    

    对我来说,这看起来非常不合适,错误处理代码是实际业务代码的三倍,但另一方面,我如何处理相互依赖的多个语句,这意味着Statement1是Statement2的先决条件,等等?

    我还对适当的资源清理感兴趣,即使是Python也可以自己管理它。

    谢谢,汤姆

    7 回复  |  直到 17 年前
        1
  •  24
  •   dbr    17 年前

    不使用try/except的else块,只需在出错时返回:

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

    这是一个可读性很好的蟒蛇……

    另一种方法是,不要担心具体的实现,例如,决定代码的外观。

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

    然后编写 message() 方法,捕获您期望的任何错误,并提升您自己的自定义错误,并在相关的地方进行处理。你的班级可能看起来像……

    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")
    
        2
  •  12
  •   David Seiler    17 年前

    一般来说,您希望使用尽可能少的try块,通过它们抛出的异常类型来区分失败条件。例如,以下是我对您发布的代码的重构:

    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
    

    这里,我们使用的事实是smtplib.smtp()、server.login()和server.sendmail()都抛出了不同的异常来扁平try-catch块的树。在finally块中,我们显式测试服务器,以避免对nil对象调用quit()。

    我们也可以用三个 相继的 尝试catch块,如果有重叠的异常情况需要单独处理,则在异常条件中返回false:

    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
    

    这并不是很好,因为您必须在多个地方杀死服务器,但是现在我们可以在不同的地方以不同的方式处理特定的异常类型,而不需要维护任何额外的状态。

        3
  •  3
  •   Peter Ericson    17 年前

    如果是我,我可能会做如下的事情:

    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
    

    捕获并调试所有错误,成功时返回值==true,如果进行了初始连接,则服务器连接将被正确清除。

        4
  •  1
  •   Ralph    17 年前

    只需使用一个试块就可以了。这正是他们 设计用于:仅当上一个语句 语句没有引发异常。资源清理方面, 如果需要清理的话,也许你可以检查一下资源 (例如myfile.is_open(),…)这确实增加了一些额外的条件,但是 只有在特殊情况下才能执行。处理这个案子 因为不同的原因,同样的例外会被提出,你 应该能够从异常中检索原因。

    我建议这样的代码:

    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
    

    错误处理代码超过业务代码的情况并不少见。正确的错误处理可能很复杂。 但是为了提高可维护性,将业务代码与错误处理代码分开是很有帮助的。

        5
  •  1
  •   Robert Rossney    17 年前

    我会尝试这样的方法:

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

    所有方法(包括 send_message ,really)遵循相同的协议:如果成功,它们将返回true,除非它们实际 手柄 一个例外,他们不设陷阱。此协议还可以处理方法需要指示失败而不引发异常的情况。(如果方法失败的唯一方法是引发异常,这将简化协议。如果您必须在失败的方法之外处理许多非异常失败状态,那么您可能有一个尚未解决的设计问题。)

    这种方法的缺点是所有的方法都必须使用相同的参数。我选择了“无”,期望我所抛弃的方法最终会操纵类成员。

    不过,这种方法的好处是相当可观的。首先,您可以向流程中添加几十个方法,而无需 森德信息 变得更加复杂。

    你也可以疯狂地做这样的事情:

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

    …尽管在这一点上,我可能会对自己说,“self,你在不创建命令类的情况下非常努力地工作命令模式。也许现在是时候了。”

        6
  •  0
  •   Jacob B    17 年前

    为什么不试一下:拦网?这样,如果发现任何异常,您将一直到异常。只要不同步骤的所有异常都是不同的,您总是可以知道触发异常的是哪个部分。

        7
  •  0
  •   Ryan Christensen    17 年前

    我喜欢大卫的回答,但是如果你被困在服务器异常上,你也可以检查服务器是否为无或状态。我稍微把这个方法展平了一点,它仍然是一个虽然不完美但在底部的逻辑中更可读的方法。

    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 
    
    推荐文章