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

29 投票
7 回答
23578 浏览
提问于 2025-04-15 12:12

我搜索了其他帖子,因为我觉得这是一个相当常见的问题,但我找到的所有关于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

在我看来,这看起来非常不符合Python的风格,而且错误处理的代码是实际业务代码的三倍,但另一方面,我该如何处理几个相互依赖的语句呢?也就是说,语句1是语句2的前提,依此类推。

我也对正确的资源清理感兴趣,即使Python也能自己处理这个问题。

谢谢,汤姆

7 个回答

3

如果是我,我可能会这样做:

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,如果最开始的连接成功,服务器的连接也会被妥善处理。

12

一般来说,你希望尽量少用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块中,我们明确检查server,以避免在一个空对象上调用quit()。

我们也可以使用三个顺序的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

这样做虽然不太优雅,因为你需要在多个地方关闭服务器,但现在我们可以在不同的地方以不同的方式处理特定的异常类型,而不需要维护额外的状态。

28

与其使用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()

这样写既清晰又符合Python的风格。

另一种做法是,不用太担心具体的实现细节,而是先决定你希望代码的样子,比如说..

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

撰写回答