Подтвердить что ты не робот

Правильный способ обработки исключений в Python?

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

Спасибо, Том

4b9b3361

Ответ 1

Вместо использования блока 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()

Это прекрасно читаемое и 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")

Ответ 2

В общем, вы хотите использовать как можно меньше блоков 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

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

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

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

Ответ 4

Просто использование одного блока 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

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

Ответ 5

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

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. Возможно, сейчас самое время".

Ответ 6

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

Ответ 7

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

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