2009-06-12 4 views
26

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

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

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

Спасибо, Том

+1

спасибо за редактирование dbr, но, пожалуйста, не редактируйте то, что вы не уверены в себе. Я отредактировал критерии возврата к критерию, который действительно является единственным, поскольку множественное число не имеет смысла там, где вы его редактировали. – Tom

+0

Opps, извините за это (хм, не думаю, что я когда-либо слышал единство критериев раньше ..) – dbr

ответ

24

Вместо использования 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() метода, са tching любые ошибки, которые вы ожидаете, и поднимаете свой собственный пользовательский интерфейс и обрабатываете его там, где это необходимо. Ваш класс может выглядеть примерно так ..

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

+1 Мне очень нравится, как вы решили, что «библиотека использует только одно исключение для всего» проблема –

+1

В вашем первом примере send_message() всегда будет возвращаться после server.login() и никогда не отправлять сообщение. Я не думаю, что для этого утверждения должно быть окончательно. – mhawke

+1

, то теперь это сводится к принципиальному вопросу.ваш первый код в основном такой же, как у меня, за исключением того, что вы просто не вставляете исключения, как я сделал в «else» -tree, что было предложено в документах python. какой из них лучше? в документах указано, что else всегда должно быть предпочтительным, а не дополнительными инструкциями в блоке try. его в основном тот же вопрос с if: лучше ли возвращаться перед другим, если, или лучше гнездо ifs условно. – Tom

0

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

+0

, потому что один большой блок try не даст вам возможность позаботиться о ressources. Например: statement1 работал и открыл файл, но оператор 2 выдает исключение. Вы не можете обработать это в одном единственном блоке finally, потому что вы никогда не знаете, действительно ли был выделен ресурс или нет. Кроме того, некоторые шаги могут запускать те же Исключения, поэтому он не может определить, что пошло не так позже, и вы не можете печатать ошибки, потому что вы не уверены, какой оператор действительно не удалось. – Tom

+0

Вложенные блоки, то? http://docs.python.org/whatsnew/2.5.html#pe p-343 –

+0

К сожалению, я не могу полагаться на методы __enter__ и __exit__, которые будут определены для всех операций, которые я буду использовать, поэтому всегда может не работать. – Tom

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

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

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

+5

точка, как указано выше, что они не бросают отдельные исключения, поэтому она не может быть тривиально сплющена. Если ваше соединение прервется, прежде чем вы сможете выполнить аутентификацию, сервер.login и server.sendMail могут выдать одно и то же исключение («сначала подключиться к серверу») Но, как я уже сказал выше, я не ищу решение для этого конкретного проблема. Меня больше интересует общий подход, как решить эту проблему. Ваш второй подход - это в основном мой код без «else». его более симпатичный, хотя я должен признать;) – Tom

+1

Остерегайтесь блока finally - вам нужно установить сервер None перед блоком, чтобы избежать случайного ссылки на несуществующую переменную. –

+0

@Tom +1, поэтому я не предлагал это решение. – Unknown

0

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

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

как в server_login, так и в send_mail вы можете избежать локальной переменной, потому что, наконец, она будет выполняться, даже если вы используете «возврат» в блоке try или except :) вы можете просто вернуть True в блок try, а False - в исключение вместо сохранения состояния в локальном var. – Tom

1

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

Я не согласен, потому что, как я уже говорил несколько раз выше, сообщения об ошибках будут неоднозначными, так как два разных вызова функций, например, login и sendmail могут вызывать одно и то же исключение. Если вы хотите распечатать пользователю или вашему журналу: «login() не удалось из-за xyz» или «sendmail() не удалось из-за xyz», это невозможно, потому что оба вызова могут привести к одному и тому же исключению. Я хочу, чтобы подробное обращение с тем, что пошло не так для целей ведения журнала. – Tom

+0

Исключение должно быть в состоянии предоставить свои данные. Например. вы можете использовать «except gaierror, (code, message):», вместо простого «except gaierror:». Затем вы получите код ошибки и сообщение об ошибке и можете использовать их для подробной обработки ошибок, например. если код == 11001: напечатать «неизвестное имя хоста:», сообщение – Ralph

+0

Я думаю, вы полностью не поняли, что я пытался сказать. Попробуйте следующее: сделайте себе объект SMTP, а затем попробуйте smtp.login() без подключения, а затем smtp.sendmail() без подключения, вы увидите, что они выбрасывают 100% идентичные исключения, которые вы не можете отличить ни от msg, ни от по errno – Tom

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 

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

+0

, это выглядит интуитивно понятным для меня, и это, вероятно, также будет выглядеть так, как в java, потому что у вас нет роскоши «else». У вас также не было этого до python2.5 iirc. Это было введено в документах, чтобы избежать больших блоков try, но все равно сохраняйте группировку красивой и очевидной, поэтому весь код, который принадлежит вместе, будет по-прежнему находиться в одной и той же попытке ... кроме ... else block, не выдувая часть try из proprtions. поскольку питоны так привязаны к их руководствам по стилю и бодростям, я решил, что это будет правильный путь. – Tom

1

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

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

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

Потенциал этого подхода значителен. Во-первых, вы можете добавить несколько десятков методов в процесс, если 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. Возможно, сейчас самое время.«

Смежные вопросы