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