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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/984526/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-03 21:12:30  来源:igfitidea点击:

Correct way of handling exceptions in Python?

pythonexception

提问by Tom

I have searched for other posts, as I felt this is a rather common problem, but all other Python exception questions I have found didn't reflect my problem.

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

I will try to be as specific here as I can, so I will give a direct example. And pleeeeease do not post any workarounds for this specific problem. I am not specifically interested how you can send an email much nicer with xyz. I want to know how you generally deal with dependent, error prone statements.

我将在这里尽可能具体,所以我将举一个直接的例子。pleeeeease 不会针对此特定问题发布任何解决方法。我对如何使用 xyz 发送更好的电子邮件并不特别感兴趣。我想知道您通常如何处理依赖的、容易出错的语句。

My question is, how to handle exceptions nicely, ones that depend on one another, meaning: Only if the first step was successful, try the next, and so on. One more criterion is: All exceptions have to be caught, this code has to be robust.

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

For your consideration, an example:

举个例子供大家参考:

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

This looks extremely unpythonic to me, and the error handling code is triple the real business code, but on the other hand how can I handle several statements that are dependent on one another, meaning statement1 is prerequisite for statement2 and so on?

这对我来说看起来非常不pythonic,错误处理代码是实际业务代码的三倍,但另一方面,我如何处理几个相互依赖的语句,这意味着 statement1 是 statement2 的先决条件,依此类推?

I am also interested in proper resource cleanup, even Python can manage that for itself.

我也对适当的资源清理感兴趣,甚至 Python 也可以自行管理。

Thanks, Tom

谢谢,汤姆

采纳答案by dbr

Instead of using the try/except's else block, you could simply return when it errors:

除了使用 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()

That's perfectly readable and Pythonic..

这是完全可读和 Pythonic..

Another way of doing this is, rather than worry about the specific implementation, decide how you want your code to look, for example..

这样做的另一种方法是,不要担心具体的实现,而是决定您希望代码的外观,例如..

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

Then write the code for the message()method, catching any errors you expect, and raising your own custom one, and handle that where it's relevant. Your class may look something like..

然后为该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")

回答by David Seiler

In general, you want to use as few try blocks as possible, distinguishing failure conditions by the kinds of exceptions they throw. For instance, here's my refactoring of the code you posted:

通常,您希望使用尽可能少的 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

Here, we use the fact that smtplib.SMTP(), server.login(), and server.sendmail() all throw different exceptions to flatten the tree of try-catch blocks. In the finally block we test server explicitly to avoid invoking quit() on the nil object.

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

We could also use three sequentialtry-catch blocks, returning False in the exception conditions, if there are overlapping exception cases that need to be handled separately:

我们也可以使用三个连续的try-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

This isn't quite as nice, as you have to kill the server in more than one place, but now we can handle specific exception types different ways in different places without maintaining any extra state.

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

回答by Peter Ericson

If it was me I would probably do something like the following:

如果是我,我可能会执行以下操作:

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

All errors are caught and debugged, the return value == True on success, and the server connection is properly cleaned up if the initial connection is made.

捕获并调试所有错误,成功时返回值 == True,如果建立初始连接,则正确清理服务器连接。

回答by Robert Rossney

I would try something like this:

我会尝试这样的事情:

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

All of the methods (including send_message, really) follow the same protocol: they return True if they succeeded, and unless they actually handlean exception, they don't trap it. This protocol also makes it possible to handle the case where a method needs to indicate that it failed without raising an exception. (If the only way your methods fail is by raising an exception, that simplifies the protocol. If you're having to deal with a lot of non-exception failure states outside of the method that failed, you probably have a design problem that you haven't worked out yet.)

所有的方法(包括send_message,真的)都遵循相同的协议:如果成功,它们返回 True,除非它们真的处理了异常,否则它们不会捕获它。该协议还可以处理方法需要在不引发异常的情况下表明它失败的情况。(如果您的方法失败的唯一方式是引发异常,则简化了协议。如果您必须处理失败方法之外的许多非异常失败状态,则您可能遇到了设计问题还没有解决。)

The downside of this approach is that all of the methods have to use the same arguments. I've opted for none, with the expectation that the methods I've stubbed out will end up manipulating class members.

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

The upside of this approach is considerable, though. First, you can add dozens of methods to the process without send_messagegetting any more complex.

不过,这种方法的好处是相当大的。首先,您可以向流程添加数十种方法,而不会send_message变得更加复杂。

You can also go crazy and do something like this:

你也可以发疯,做这样的事情:

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

...though at that point, I might say to myself, "self, you're working the Command pattern pretty hard without creating a Command class. Maybe now is the time."

...虽然那时,我可能会对自己说,“我自己,你在没有创建 Command 类的情况下非常努力地使用 Command 模式。也许现在是时候了。”

回答by Ralph

Just using one try-block is the way to go. This is exactly what they are designed for: only execute the next statement if the previous statement did not throw an exception. As for the resource clean-ups, maybe you can check the resource if it needs to be cleaned up (e.g. myfile.is_open(), ...) This does add some extra conditions, but they will only be executed in the exceptional case. To handle the case that the same Exception can be raised for different reasons, you should be able to retrieve the reason from the Exception.

只需使用一个 try-block 即可。这正是它们的设计目的:只有在前一条语句没有抛出异常时才执行下一条语句。至于资源清理,如果需要清理的资源可以检查一下(例如myfile.is_open(),...)这确实增加了一些额外的条件,但它们只会在特殊情况下执行. 要处理由于不同原因可能引发相同 Exception 的情况,您应该能够从 Exception 中检索原因。

I suggest code like this:

我建议这样的代码:

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

It is no uncommon, that error handling code exceeds business code. Correct error handling can be complex. But to increase maintainability it helps to separate the business code from the error handling code.

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

回答by Ryan Christensen

I like David's answer but if you are stuck on the server exceptions you can also check for server if is None or states. I flattened out the method a bit bit it is still a but unpythonic looking but more readable in the logic at the bottom.

我喜欢 David 的回答,但如果您遇到服务器异常,您还可以检查服务器是否为 None 或状态。我稍微扁平化了这个方法,它仍然是一个但不是pythonic的外观,但在底部的逻辑中更具可读性。

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 

回答by Jacob B

Why not one big try: block? This way, if any exception is caught, you'll go all the way to the except. And as long as all the exceptions for the different steps are different, you can always tell which part it was that fired the exception.

为什么不进行一次大尝试:阻止?这样,如果捕获到任何异常,您将一直走到except。只要不同步骤的所有异常都是不同的,你总是可以知道是哪个部分触发了异常。