Правильный способ обработки исключений в Python? - PullRequest
27 голосов
/ 12 июня 2009

Я искал другие сообщения, так как чувствовал, что это довольно распространенная проблема, но все остальные вопросы об исключениях Python, которые я нашел, не отражали мою проблему.

Я постараюсь быть здесь настолько конкретным, насколько смогу, поэтому приведу прямой пример. И pleeeeease не публикуют какие-либо обходные пути для этой конкретной проблемы. Меня не особо интересует, как вы можете отправить электронное письмо намного приятнее с помощью 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

Для меня это выглядит крайне непитонным, и код обработки ошибок в три раза больше реального бизнес-кода, но с другой стороны, как я могу обрабатывать несколько операторов, которые зависят друг от друга, что означает, что оператор1 является необходимым условием для оператора2 и так далее?

Я также заинтересован в правильной очистке ресурсов, даже Python может справиться с этим самостоятельно.

Спасибо, Том

Ответы [ 7 ]

24 голосов
/ 12 июня 2009

Вместо использования блока else / try, вы можете просто вернуть, когда он выдаст ошибку:

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

Это отлично читается и Pythonic ..

Другой способ сделать это, например, не беспокоиться о конкретной реализации, решить, как вы хотите, чтобы ваш код выглядел, например ..

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")
12 голосов
/ 12 июня 2009

Как правило, вы хотите использовать как можно меньше блоков 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 мы явно тестируем сервер, чтобы избежать вызова quit () для объекта nil.

Мы также можем использовать три последовательных блока 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

Это не так хорошо, так как вам приходится убивать сервер более чем в одном месте, но теперь мы можем обрабатывать определенные типы исключений различными способами в разных местах без сохранения какого-либо дополнительного состояния.

3 голосов
/ 12 июня 2009

Если бы это был я, я бы сделал что-то вроде следующего:

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

Все ошибки перехватываются и отлаживаются, возвращаемое значение == Истина при успехе, и соединение с сервером корректно очищается, если установлено первоначальное соединение.

1 голос
/ 12 июня 2009

Я бы попробовал что-то вроде этого:

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, на самом деле) следуют одному и тому же протоколу: они возвращают True, если им это удалось, и если они на самом деле не обработают исключение, они его не перехватывают. Этот протокол также позволяет обрабатывать случай, когда метод должен указать, что он потерпел неудачу, не вызывая исключения. (Если единственным способом сбоя ваших методов является создание исключения, это упрощает протокол. Если вам приходится иметь дело со многими состояниями сбоя неисключения вне метода, который не удался, вы, вероятно, столкнулись с проблемой проектирования, которая еще не сработало.)

Недостатком этого подхода является то, что все методы должны использовать одни и те же аргументы. Я выбрал ни одного, ожидая, что методы, которые я заглушил, в конечном итоге будут манипулировать членами класса.

Преимущества этого подхода значительны, однако. Во-первых, вы можете добавить к процессу десятки методов, не усложняя send_message.

Вы также можете сходить с ума и делать что-то вроде этого:

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

... хотя в этот момент я мог бы сказать себе: «Сам, ты довольно тяжело работаешь с шаблоном Command, не создавая класс Command. Возможно, сейчас самое время».

1 голос
/ 12 июня 2009

Просто используйте один блок try. Это именно то, что они предназначены для: только выполнения следующего оператора, если предыдущий Заявление не выкинуло исключение. Что касается очистки ресурсов, может быть, вы можете проверить ресурс, если он должен быть очищен (например, 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

Нередко код обработки ошибок превышает бизнес-код. Правильная обработка ошибок может быть сложной. Но для повышения удобства обслуживания помогает отделить бизнес-код от кода обработки ошибок.

0 голосов
/ 12 июня 2009

Мне нравится ответ Дэвида, но если вы застряли на исключениях сервера, вы также можете проверить сервер, если установлено значение Нет или указано состояние. Я немного сгладил метод, но он все еще не выглядит, но более читабелен в логике внизу.

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 голосов
/ 12 июня 2009

Почему бы не одна большая попытка: блок? Таким образом, если обнаружится какое-либо исключение, вы пройдете весь путь до исключения. И пока все исключения для разных этапов различны, вы всегда можете сказать, какая именно часть вызвала исключение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...