Содержание:развернуть
- Как устроен механизм исключений
- Как обрабатывать исключения в Python (try except)
-
As — сохраняет ошибку в переменную
-
Finally — выполняется всегда
-
Else — выполняется когда исключение не было вызвано
-
Несколько блоков except
-
Несколько типов исключений в одном блоке except
-
Raise — самостоятельный вызов исключений
-
Как пропустить ошибку
- Исключения в lambda функциях
- 20 типов встроенных исключений в Python
- Как создать свой тип Exception
Программа, написанная на языке Python, останавливается сразу как обнаружит ошибку. Ошибки могут быть (как минимум) двух типов:
- Синтаксические ошибки — возникают, когда написанное выражение не соответствует правилам языка (например, написана лишняя скобка);
- Исключения — возникают во время выполнения программы (например, при делении на ноль).
Синтаксические ошибки исправить просто (если вы используете IDE, он их подсветит). А вот с исключениями всё немного сложнее — не всегда при написании программы можно сказать возникнет или нет в данном месте исключение. Чтобы приложение продолжило работу при возникновении проблем, такие ошибки нужно перехватывать и обрабатывать с помощью блока try/except.
Как устроен механизм исключений
В Python есть встроенные исключения, которые появляются после того как приложение находит ошибку. В этом случае текущий процесс временно приостанавливается и передает ошибку на уровень вверх до тех пор, пока она не будет обработано. Если ошибка не будет обработана, программа прекратит свою работу (а в консоли мы увидим Traceback с подробным описанием ошибки).
💁♂️ Пример: напишем скрипт, в котором функция ожидает число, а мы передаём сроку (это вызовет исключение «TypeError»):
def b(value):
print("-> b")
print(value + 1) # ошибка тут
def a(value):
print("-> a")
b(value)
a("10")
> -> a
> -> b
> Traceback (most recent call last):
> File "test.py", line 11, in <module>
> a("10")
> File "test.py", line 8, in a
> b(value)
> File "test.py", line 3, in b
> print(value + 1)
> TypeError: can only concatenate str (not "int") to str
В данном примере мы запускаем файл «test.py» (через консоль). Вызывается функция «a«, внутри которой вызывается функция «b«. Все работает хорошо до сточки print(value + 1). Тут интерпретатор понимает, что нельзя конкатенировать строку с числом, останавливает выполнение программы и вызывает исключение «TypeError».
Далее ошибка передается по цепочке в обратном направлении: «b» → «a» → «test.py«. Так как в данном примере мы не позаботились обработать эту ошибку, вся информация по ошибке отобразится в консоли в виде Traceback.
Traceback (трассировка) — это отчёт, содержащий вызовы функций, выполненные в определенный момент. Трассировка помогает узнать, что пошло не так и в каком месте это произошло.
Traceback лучше читать снизу вверх ↑
В нашем примере Traceback содержится следующую информацию (читаем снизу вверх):
TypeError— тип ошибки (означает, что операция не может быть выполнена с переменной этого типа);can only concatenate str (not "int") to str— подробное описание ошибки (конкатенировать можно только строку со строкой);- Стек вызова функций (1-я линия — место, 2-я линия — код). В нашем примере видно, что в файле «test.py» на 11-й линии был вызов функции «a» со строковым аргументом «10». Далее был вызов функции «b».
print(value + 1)это последнее, что было выполнено — тут и произошла ошибка. most recent call last— означает, что самый последний вызов будет отображаться последним в стеке (в нашем примере последним выполнилсяprint(value + 1)).
В Python ошибку можно перехватить, обработать, и продолжить выполнение программы — для этого используется конструкция try ... except ....
Как обрабатывать исключения в Python (try except)
В Python исключения обрабатываются с помощью блоков try/except. Для этого операция, которая может вызвать исключение, помещается внутрь блока try. А код, который должен быть выполнен при возникновении ошибки, находится внутри except.
Например, вот как можно обработать ошибку деления на ноль:
try:
a = 7 / 0
except:
print('Ошибка! Деление на 0')
Здесь в блоке try находится код a = 7 / 0 — при попытке его выполнить возникнет исключение и выполнится код в блоке except (то есть будет выведено сообщение «Ошибка! Деление на 0»). После этого программа продолжит свое выполнение.
💭 PEP 8 рекомендует, по возможности, указывать конкретный тип исключения после ключевого слова except (чтобы перехватывать и обрабатывать конкретные исключения):
try:
a = 7 / 0
except ZeroDivisionError:
print('Ошибка! Деление на 0')
Однако если вы хотите перехватывать все исключения, которые сигнализируют об ошибках программы, используйте тип исключения Exception:
try:
a = 7 / 0
except Exception:
print('Любая ошибка!')
As — сохраняет ошибку в переменную
Перехваченная ошибка представляет собой объект класса, унаследованного от «BaseException». С помощью ключевого слова as можно записать этот объект в переменную, чтобы обратиться к нему внутри блока except:
try:
file = open('ok123.txt', 'r')
except FileNotFoundError as e:
print(e)
> [Errno 2] No such file or directory: 'ok123.txt'
В примере выше мы обращаемся к объекту класса «FileNotFoundError» (при выводе на экран через print отобразится строка с полным описанием ошибки).
У каждого объекта есть поля, к которым можно обращаться (например если нужно логировать ошибку в собственном формате):
import datetime
now = datetime.datetime.now().strftime("%d-%m-%Y %H:%M:%S")
try:
file = open('ok123.txt', 'r')
except FileNotFoundError as e:
print(f"{now} [FileNotFoundError]: {e.strerror}, filename: {e.filename}")
> 20-11-2021 18:42:01 [FileNotFoundError]: No such file or directory, filename: ok123.txt
Finally — выполняется всегда
При обработке исключений можно после блока try использовать блок finally. Он похож на блок except, но команды, написанные внутри него, выполняются обязательно. Если в блоке try не возникнет исключения, то блок finally выполнится так же, как и при наличии ошибки, и программа возобновит свою работу.
Обычно try/except используется для перехвата исключений и восстановления нормальной работы приложения, а try/finally для того, чтобы гарантировать выполнение определенных действий (например, для закрытия внешних ресурсов, таких как ранее открытые файлы).
В следующем примере откроем файл и обратимся к несуществующей строке:
file = open('ok.txt', 'r')
try:
lines = file.readlines()
print(lines[5])
finally:
file.close()
if file.closed:
print("файл закрыт!")
> файл закрыт!
> Traceback (most recent call last):
> File "test.py", line 5, in <module>
> print(lines[5])
> IndexError: list index out of range
Даже после исключения «IndexError», сработал код в секции finally, который закрыл файл.
p.s. данный пример создан для демонстрации, в реальном проекте для работы с файлами лучше использовать менеджер контекста with.
Также можно использовать одновременно три блока try/except/finally. В этом случае:
- в
try— код, который может вызвать исключения; - в
except— код, который должен выполниться при возникновении исключения; - в
finally— код, который должен выполниться в любом случае.
def sum(a, b):
res = 0
try:
res = a + b
except TypeError:
res = int(a) + int(b)
finally:
print(f"a = {a}, b = {b}, res = {res}")
sum(1, "2")
> a = 1, b = 2, res = 3
Else — выполняется когда исключение не было вызвано
Иногда нужно выполнить определенные действия, когда код внутри блока try не вызвал исключения. Для этого используется блок else.
Допустим нужно вывести результат деления двух чисел и обработать исключения в случае попытки деления на ноль:
b = int(input('b = '))
c = int(input('c = '))
try:
a = b / c
except ZeroDivisionError:
print('Ошибка! Деление на 0')
else:
print(f"a = {a}")
> b = 10
> c = 1
> a = 10.0
В этом случае, если пользователь присвоит переменной «с» ноль, то появится исключение и будет выведено сообщение «‘Ошибка! Деление на 0′», а код внутри блока else выполняться не будет. Если ошибки не будет, то на экране появятся результаты деления.
Несколько блоков except
В программе может возникнуть несколько исключений, например:
- Ошибка преобразования введенных значений к типу
float(«ValueError»); - Деление на ноль («ZeroDivisionError»).
В Python, чтобы по-разному обрабатывать разные типы ошибок, создают несколько блоков except:
try:
b = float(input('b = '))
c = float(input('c = '))
a = b / c
except ZeroDivisionError:
print('Ошибка! Деление на 0')
except ValueError:
print('Число введено неверно')
else:
print(f"a = {a}")
> b = 10
> c = 0
> Ошибка! Деление на 0
> b = 10
> c = питон
> Число введено неверно
Теперь для разных типов ошибок есть свой обработчик.
Несколько типов исключений в одном блоке except
Можно также обрабатывать в одном блоке except сразу несколько исключений. Для этого они записываются в круглых скобках, через запятую сразу после ключевого слова except. Чтобы обработать сообщения «ZeroDivisionError» и «ValueError» в одном блоке записываем их следующим образом:
try:
b = float(input('b = '))
c = float(input('c = '))
a = b / c
except (ZeroDivisionError, ValueError) as er:
print(er)
else:
print('a = ', a)
При этом переменной er присваивается объект того исключения, которое было вызвано. В результате на экран выводятся сведения о конкретной ошибке.
Raise — самостоятельный вызов исключений
Исключения можно генерировать самостоятельно — для этого нужно запустить оператор raise.
min = 100
if min > 10:
raise Exception('min must be less than 10')
> Traceback (most recent call last):
> File "test.py", line 3, in <module>
> raise Exception('min value must be less than 10')
> Exception: min must be less than 10
Перехватываются такие сообщения точно так же, как и остальные:
min = 100
try:
if min > 10:
raise Exception('min must be less than 10')
except Exception:
print('Моя ошибка')
> Моя ошибка
Кроме того, ошибку можно обработать в блоке except и пробросить дальше (вверх по стеку) с помощью raise:
min = 100
try:
if min > 10:
raise Exception('min must be less than 10')
except Exception:
print('Моя ошибка')
raise
> Моя ошибка
> Traceback (most recent call last):
> File "test.py", line 5, in <module>
> raise Exception('min must be less than 10')
> Exception: min must be less than 10
Как пропустить ошибку
Иногда ошибку обрабатывать не нужно. В этом случае ее можно пропустить с помощью pass:
try:
a = 7 / 0
except ZeroDivisionError:
pass
Исключения в lambda функциях
Обрабатывать исключения внутри lambda функций нельзя (так как lambda записывается в виде одного выражения). В этом случае нужно использовать именованную функцию.
20 типов встроенных исключений в Python
Иерархия классов для встроенных исключений в Python выглядит так:
BaseException
SystemExit
KeyboardInterrupt
GeneratorExit
Exception
ArithmeticError
AssertionError
...
...
...
ValueError
Warning
Все исключения в Python наследуются от базового BaseException:
SystemExit— системное исключение, вызываемое функциейsys.exit()во время выхода из приложения;KeyboardInterrupt— возникает при завершении программы пользователем (чаще всего при нажатии клавиш Ctrl+C);GeneratorExit— вызывается методомcloseобъектаgenerator;Exception— исключения, которые можно и нужно обрабатывать (предыдущие были системными и их трогать не рекомендуется).
От Exception наследуются:
1 StopIteration — вызывается функцией next в том случае если в итераторе закончились элементы;
2 ArithmeticError — ошибки, возникающие при вычислении, бывают следующие типы:
FloatingPointError— ошибки при выполнении вычислений с плавающей точкой (встречаются редко);OverflowError— результат вычислений большой для текущего представления (не появляется при операциях с целыми числами, но может появиться в некоторых других случаях);ZeroDivisionError— возникает при попытке деления на ноль.
3 AssertionError — выражение, используемое в функции assert неверно;
4 AttributeError — у объекта отсутствует нужный атрибут;
5 BufferError — операция, для выполнения которой требуется буфер, не выполнена;
6 EOFError — ошибка чтения из файла;
7 ImportError — ошибка импортирования модуля;
8 LookupError — неверный индекс, делится на два типа:
IndexError— индекс выходит за пределы диапазона элементов;KeyError— индекс отсутствует (для словарей, множеств и подобных объектов);
9 MemoryError — память переполнена;
10 NameError — отсутствует переменная с данным именем;
11 OSError — исключения, генерируемые операционной системой:
ChildProcessError— ошибки, связанные с выполнением дочернего процесса;ConnectionError— исключения связанные с подключениями (BrokenPipeError, ConnectionResetError, ConnectionRefusedError, ConnectionAbortedError);FileExistsError— возникает при попытке создания уже существующего файла или директории;FileNotFoundError— генерируется при попытке обращения к несуществующему файлу;InterruptedError— возникает в том случае если системный вызов был прерван внешним сигналом;IsADirectoryError— программа обращается к файлу, а это директория;NotADirectoryError— приложение обращается к директории, а это файл;PermissionError— прав доступа недостаточно для выполнения операции;ProcessLookupError— процесс, к которому обращается приложение не запущен или отсутствует;TimeoutError— время ожидания истекло;
12 ReferenceError — попытка доступа к объекту с помощью слабой ссылки, когда объект не существует;
13 RuntimeError — генерируется в случае, когда исключение не может быть классифицировано или не подпадает под любую другую категорию;
14 NotImplementedError — абстрактные методы класса нуждаются в переопределении;
15 SyntaxError — ошибка синтаксиса;
16 SystemError — сигнализирует о внутренне ошибке;
17 TypeError — операция не может быть выполнена с переменной этого типа;
18 ValueError — возникает когда в функцию передается объект правильного типа, но имеющий некорректное значение;
19 UnicodeError — исключение связанное с кодирование текста в unicode, бывает трех видов:
UnicodeEncodeError— ошибка кодирования;UnicodeDecodeError— ошибка декодирования;UnicodeTranslateError— ошибка переводаunicode.
20 Warning — предупреждение, некритическая ошибка.
💭 Посмотреть всю цепочку наследования конкретного типа исключения можно с помощью модуля inspect:
import inspect
print(inspect.getmro(TimeoutError))
> (<class 'TimeoutError'>, <class 'OSError'>, <class 'Exception'>, <class 'BaseException'>, <class 'object'>)
📄 Подробное описание всех классов встроенных исключений в Python смотрите в официальной документации.
Как создать свой тип Exception
В Python можно создавать свои исключения. При этом есть одно обязательное условие: они должны быть потомками класса Exception:
class MyError(Exception):
def __init__(self, text):
self.txt = text
try:
raise MyError('Моя ошибка')
except MyError as er:
print(er)
> Моя ошибка
С помощью try/except контролируются и обрабатываются ошибки в приложении. Это особенно актуально для критически важных частей программы, где любые «падения» недопустимы (или могут привести к негативным последствиям). Например, если программа работает как «демон», падение приведет к полной остановке её работы. Или, например, при временном сбое соединения с базой данных, программа также прервёт своё выполнение (хотя можно было отловить ошибку и попробовать соединиться в БД заново).
Вместе с try/except можно использовать дополнительные блоки. Если использовать все блоки описанные в статье, то код будет выглядеть так:
try:
# попробуем что-то сделать
except (ZeroDivisionError, ValueError) as e:
# обрабатываем исключения типа ZeroDivisionError или ValueError
except Exception as e:
# исключение не ZeroDivisionError и не ValueError
# поэтому обрабатываем исключение общего типа (унаследованное от Exception)
# сюда не сходят исключения типа GeneratorExit, KeyboardInterrupt, SystemExit
else:
# этот блок выполняется, если нет исключений
# если в этом блоке сделать return, он не будет вызван, пока не выполнился блок finally
finally:
# этот блок выполняется всегда, даже если нет исключений else будет проигнорирован
# если в этом блоке сделать return, то return в блоке
Подробнее о работе с исключениями в Python можно ознакомиться в официальной документации.

Одним из недостатков гибких языков, таких как Python, является предположение, что если что-то работает, то скорее всего оно сделано правильно. Я хочу написать скромное руководство по эффективному использованию исключений в Python, правильной их обработке и логировании.
Эффективная обработка исключений
Введение
Давайте рассмотрим следующую систему. У нас есть микросервис, который отвечает за:
· Прослушивание событий о новом заказе;
· Получение заказа из базы данных;
· Проверку состояния принтера;
· Печать квитанции;
· Отправка квитанции в налоговую систему (IRS).

В любой момент может сломаться что угодно. У вас могут возникнуть проблемы с объектом заказа, в котором может не быть нужной информации, или в принтере может закончиться бумага, или же сервис налоговой не будет работать, и вы не сможете синхронизировать с ними квитанцию об оплате, а может быть ваша база данных окажется недоступна.
Ваша задача правильно и проактивно реагировать на любую ситуацию, чтобы избежать ошибок при обработке новых заказов.
И примерно вот такой код на этот случай пишут люди (он, конечно, работает, но плохо и неэффективно):
class OrderService:
def emit(self, order_id: str) -> dict:
try:
order_status = status_service.get_order_status(order_id)
except Exception as e:
logger.exception(
f"Order {order_id} was not found in db "
f"to emit. Error: {e}."
)
raise e
(
is_order_locked_in_emission,
seconds_in_emission,
) = status_service.is_order_locked_in_emission(order_id)
if is_order_locked_in_emission:
logger.info(
"Redoing emission because "
"it was locked in that state after a long time! "
f"Time spent in that state: {seconds_in_emission} seconds "
f"Order: {order_id}, "
f"order_status: {order_status.value}"
)
elif order_status == OrderStatus.EMISSION_IN_PROGRESS:
logger.info("Aborting emission request because it is already in progress!")
return {"order_id": order_id, "order_status": order_status.value}
elif order_status == OrderStatus.EMISSION_SUCCESSFUL:
logger.info(
"Aborting emission because it already happened! "
f"Order: {order_id}, "
f"order_status: {order_status.value}"
)
return {"order_id": order_id, "order_status": order_status.value}
try:
receipt_note = receipt_service.create(order_id)
except Exception as e:
logger.exception(
"Error found during emission! "
f"Order: {order_id}, "
f"exception: {e}"
)
raise e
try:
broker.emit_receipt_note(receipt_note)
except Exception as e:
logger.exception(
"Emission failed! "
f"Order: {order_id}, "
f"exception: {e}"
)
raise e
order_status = status_service.get_order_status(order_id)
return {"order_id": order_id, "order_status": order_status.value}
Сначала я сосредоточусь на том, что OrderService слишком много знает, и все эти данные делают его чем-то вроде blob, а чуть позже расскажу о правильной обработке и правильном логировании исключений.
Почему этот сервис — blob?
Этот сервис знает слишком много. Кто-то может сказать, что он знает только то, что ему нужно (то есть все шаги, связанные с формированием чека), но на самом деле он знает куда больше.
Он сосредоточен на создании ошибок (например, база данных, печать, статус заказа), а не на том, что он делает (например, извлекает, проверяет статус, генерирует, отправляет) и на том, как следует реагировать в случае сбоев.
В этом смысле мне кажется, что клиент учит сервис тому, какие исключения он может выдать. Если мы решим переиспользовать его на любом другом этапе (например, клиент захочет получить еще одну печатную копию более старого чека по заказу), мы скопируем большую часть этого кода.
Несмотря на то, что сервис работает нормально, поддерживать его трудно, и неясно, как один шаг соотносится с другим из-за повторяющихся блоков except между шагами, которые отвлекают наше внимание на вопрос «как» вместо того, чтобы думать о «когда».
Первое улучшение: делайте исключения конкретными
Давайте сначала сделаем исключения более точными и конкретными. Преимущества не видны сразу, поэтому я не буду тратить слишком много времени на объяснение этого прямо сейчас. Однако обратите внимание на то, как изменяется код.
Я выделю только то, что мы поменяли:
try:
order_status = status_service.get_order_status(order_id)
except Exception as e:
logger.exception(...)
raise OrderNotFound(order_id) from e
...
try:
...
except Exception as e:
logger.exception(...)
raise ReceiptGenerationFailed(order_id) from e
try:
broker.emit_receipt_note(receipt_note)
except Exception as e:
logger.exception(...)
raise ReceiptEmissionFailed(order_id) from e
Обратите внимание, что на этот раз я пользуюсь from e, что является правильным способом создания одного исключения из другого и сохраняет полную трассировку стека.
Второе улучшение: не лезьте не в свое дело
Теперь, когда у нас есть кастомные исключения, мы можем перейти к мысли «не учите классы тому, что может пойти не так» — они сами скажут, если это случится!
# Services
class StatusService:
def get_order_status(order_id):
try:
...
except Exception as e:
raise OrderNotFound(order_id) from e
class ReceiptService:
def create(order_id):
try:
...
except Exception as e:
raise ReceiptGenerationFailed(order_id) from e
class Broker:
def emit_receipt_note(receipt_note):
try:
...
except Exception as e:
raise ReceiptEmissionFailed(order_id) from e
# Main class
class OrderService:
def emit(self, order_id: str) -> dict:
try:
order_status = status_service.get_order_status(order_id)
(
is_order_locked_in_emission,
seconds_in_emission,
) = status_service.is_order_locked_in_emission(order_id)
if is_order_locked_in_emission:
logger.info(
"Redoing emission because "
"it was locked in that state after a long time! "
f"Time spent in that state: {seconds_in_emission} seconds "
f"Order: {order_id}, "
f"order_status: {order_status.value}"
)
elif order_status == OrderStatus.EMISSION_IN_PROGRESS:
logger.info("Aborting emission request because it is already in progress!")
return {"order_id": order_id, "order_status": order_status.value}
elif order_status == OrderStatus.EMISSION_SUCCESSFUL:
logger.info(
"Aborting emission because it already happened! "
f"Order: {order_id}, "
f"order_status: {order_status.value}"
)
return {"order_id": order_id, "order_status": order_status.value}
receipt_note = receipt_service.create(order_id)
broker.emit_receipt_note(receipt_note)
order_status = status_service.get_order_status(order_id)
except OrderNotFound as e:
logger.exception(
f"Order {order_id} was not found in db "
f"to emit. Error: {e}."
)
raise
except ReceiptGenerationFailed as e:
logger.exception(
"Error found during emission! "
f"Order: {order_id}, "
f"exception: {e}"
)
raise
except ReceiptEmissionFailed as e:
logger.exception(
"Emission failed! "
f"Order: {order_id}, "
f"exception: {e}"
)
raise
else:
return {"order_id": order_id, "order_status": order_status.value}
Как вам? Намного лучше, правда? У нас есть один блок try, который построен достаточно логично, чтобы понять, что произойдет дальше. Вы сгруппировали конкретные блоки, за исключением тех, которые помогают вам понять «когда» и крайние случаи. И, наконец, у вас есть блок else, в котором описано, что произойдет, если все отработает как надо.
Кроме того, пожалуйста, обратите внимание на то, что я сохранил инструкции raise без объявления объекта исключения. Это не опечатка. На самом деле, это правильный способ повторного вызова исключения: простой и немногословный.
Но это еще не все. Логирование продолжает меня раздражать.
Третье улучшение: улучшение логирования
Этот шаг напоминает мне принцип «говори, а не спрашивай», хотя это все же не совсем он. Вместо того, чтобы запрашивать подробности исключения и на их основе выдавать полезные сообщения, исключения должны выдавать их сами – в конце концов, я их конкретизировал!
### Exceptions
class OrderCreationException(Exception):
pass
class OrderNotFound(OrderCreationException):
def __init__(self, order_id):
self.order_id = order_id
super().__init__(
f"Order {order_id} was not found in db "
"to emit."
)
class ReceiptGenerationFailed(OrderCreationException):
def __init__(self, order_id):
self.order_id = order_id
super().__init__(
"Error found during emission! "
f"Order: {order_id}"
)
class ReceiptEmissionFailed(OrderCreationException):
def __init__(self, order_id):
self.order_id = order_id
super().__init__(
"Emission failed! "
f"Order: {order_id} "
)
### Main class
class OrderService:
def emit(self, order_id: str) -> dict:
try:
...
except OrderNotFound:
logger.exception("We got a database exception")
raise
except ReceiptGenerationFailed:
logger.exception("We got a problem generating the receipt")
raise
except ReceiptEmissionFailed:
logger.exception("Unable to emit the receipt")
raise
else:
return {"order_id": order_id, "order_status": order_status.value}
Наконец-то мои глаза чувствуют облегчение. Поменьше повторений, пожалуйста! Примите к сведению, что рекомендуемый способ выглядит так, как я написал его выше: logger.exception(«ЛЮБОЕ СООБЩЕНИЕ»). Вам даже не нужно передавать исключение, поскольку его наличие уже подразумевается. Кроме того, кастомное сообщение, которое мы определили в каждом исключении с идентификатором order_id, будет отображаться в логах, поэтому вам не нужно повторяться и не нужно оперировать внутренними данными об исключениях.
Вот пример вывода ваших логов:
❯ python3 testme.py
Unable to emit the receipt # <<-- My log message
Traceback (most recent call last):
File "/path/testme.py", line 19, in <module>
tryme()
File "/path/testme.py", line 14, in tryme
raise ReceiptEmissionFailed(order_id)
ReceiptEmissionFailed: Emission failed! Order: 10 # <<-- My exception message
Теперь всякий раз, когда я получаю это исключение, сообщение уже ясно и понятно, и мне не нужно помнить о логировании order_id, который я сгенерировал.
Последнее улучшение: упрощение
После более детального рассмотрения нашего окончательного кода, он кажется лучше, теперь его легко читать и поддерживать.
Но управляет ли OrderService бизнес-логикой? Я не думаю, что это сервис в общем смысле. Он больше похож на координацию вызовов настоящих сервисов обеспечивающих бизнес-логику, которая выглядит получше, чем паттерн facade.
Кроме того, можно заметить, что он запрашивает данные у status_service, чтобы что-то с ними сделать. (Что, на этот раз, действительно разрушает идею «Говори, а не спрашивай»).
Перейдем к упрощению.
class OrderFacade: # Renamed to match what it actually is
def emit(self, order_id: str) -> dict:
try:
# NOTE: info logging still happens inside
status_service.ensure_order_unlocked(order_id)
receipt_note = receipt_service.create(order_id)
broker.emit_receipt_note(receipt_note)
order_status = status_service.get_order_status(order_id)
except OrderAlreadyInProgress as e:
# New block
logger.info("Aborting emission request because it is already in progress!")
return {"order_id": order_id, "order_status": e.order_status.value}
except OrderAlreadyEmitted as e:
# New block
logger.info(f"Aborting emission because it already happened! {e}")
return {"order_id": order_id, "order_status": e.order_status.value}
except OrderNotFound:
logger.exception("We got a database exception")
raise
except ReceiptGenerationFailed:
logger.exception("We got a problem generating the receipt")
raise
except ReceiptEmissionFailed:
logger.exception("Unable to emit the receipt")
raise
else:
return {"order_id": order_id, "order_status": order_status.value}
Мы только что создали новый метод ensure_order_unlocked для нашего status_service, который теперь отвечает за создание исключений/логирование в случае, если что-то идет не так.
Хорошо, а теперь скажите, насколько легче теперь стало это читать?
Я могу понять все return при беглом просмотре. Я знаю, что происходит, когда все идет хорошо, и как крайние случаи могут привести к разным результатам. И все это без прокрутки взад-вперед.
Теперь этот код такой же простой, каким (в основном) должен быть любой код.
Обратите внимание, что я решил вывести объект исключения e в логах, поскольку под капотом он будет запускать str(e), который вернет сообщение об исключении. Я подумал, что было бы полезно говорить подробно, поскольку мы не используем log.exception для этого блока, поэтому сообщение об исключении не будет отображаться.
Теперь давайте разберемся с некоторыми хитростями, которые помогут вам сделать код понятным для чтения и простым в обслуживании.
Эффективное создание исключений
Всегда классифицируйте свои исключения через базовое и расширяйте все конкретные исключения от него. С помощью этой полезной практики вы можете переиспользовать логику для связанного кода.
Исключения – это объекты, которые несут в себе информацию, поэтому не стесняйтесь добавлять кастомные атрибуты, которые могут помочь вам понять, что происходит. Не позволяйте своему бизнес-коду учить вас тому, как он должен быть построен, ведь с таким количеством сообщений и деталей потерять себя становится трудно.
# Base category exception
class OrderCreationException(Exception):
pass
# Specific error with custom message. Order id is required.
class OrderNotFound(OrderCreationException):
def __init__(self, order_id):
self.order_id = order_id # custom property
super().__init__(
f"Order {order_id} was not found in db "
f"to emit."
)
# Specific error with custom message. Order id is required.
class ReceiptGenerationFailed(OrderCreationException):
def __init__(self, order_id):
self.order_id = order_id # custom property
super().__init__(
"Error found during emission! "
f"Order: {order_id}"
)
В примере выше я мог бы выйти за рамки и расширить базовый класс, чтобы всегда получать order_id, если мне это нужно. Этот совет поможет сохранить код сухим, поскольку мне не нужно быть многословным при создании исключений. Так можно использовать всего лишь одну переменную.
def func1(order_id):
raise OrderNotFound(order_id)
# instead of raise OrderNotFound(f"Can't find order {order_id}")
def func2(order_id):
raise OrderNotFound(order_id)
# instead of raise OrderNotFound(f"Can't find order {order_id}")
В тестировании также будет больше смысла, поскольку я могу сделать assert order_id через строку.
assert e.order_id == order_id
# instead of assert order_id in str(e)
Ловим и создаем исключения эффективно
Еще одна вещь, которую люди часто делают неправильно – это отлавливают и повторно создают исключения.
Согласно PEP 3134 Python, делать нужно следующим образом.
Повторное создание исключения
Обычной инструкции raise более чем достаточно.
try:
...
except CustomException as ex:
# do stuff (e.g. logging)
raise
Создание одного исключения из другого
Этот вариант особо актуален, поскольку он сохраняет всю трассировку стека и помогает вашей команде отлаживать основные проблемы.
try:
...
except CustomException as ex:
raise MyNewException() from ex
Эффективное логирование исключений
Еще один совет, который не позволит вам быть слишком многословным.
Используйте logger.exception
Вам не нужно логировать объект исключения. Функция exception логгера предназначена для использования внутри блоков except. Она уже обрабатывает трассировку стека с информацией о выполнении и отображает, какое исключение вызвало ее, с сообщением, установленном на уровне ошибки!
try:
...
except CustomException:
logger.exception("custom message")
А что, если это не ошибка?
Если по какой-то причине вы не хотите логировать исключение как ошибку, то возможно, это предупреждение или просто информация, как было показано выше.
Вы можете принять решение установить exc_info в True, если хотите сохранить трассировку стека. Кроме того, было бы неплохо использовать объект исключения внутри сообщения.
Источники
Документация Python:
· Python logging.logger.exception
· Python PEP 3134
Принципы и качество кода:
· Говори, а не спрашивай
· Паттерн facade
· Blob
— РЕГИСТРАЦИЯ
В этом руководстве мы расскажем, как обрабатывать исключения в Python с помощью try и except. Рассмотрим общий синтаксис и простые примеры, обсудим, что может пойти не так, и предложим меры по исправлению положения.
Зачастую разработчик может предугадать возникновение ошибок при работе даже синтаксически и логически правильной программы. Эти ошибки могут быть вызваны неверными входными данными или некоторыми предсказуемыми несоответствиями.
Для обработки большей части этих ошибок как исключений в Python есть блоки try и except.
Для начала разберем синтаксис операторов try и except в Python. Общий шаблон представлен ниже:
try:
# В этом блоке могут быть ошибки
except <error type>:
# Сделай это для обработки исключения;
# выполняется, если блок try выбрасывает ошибку
else:
# Сделай это, если блок try выполняется успешно, без ошибок
finally:
# Этот блок выполняется всегда
Давайте посмотрим, для чего используются разные блоки.
Блок try
Блок try — это блок кода, который вы хотите попробовать выполнить. Однако во время выполнения из-за какого-нибудь исключения могут возникнуть ошибки. Поэтому этот блок может не работать должным образом.
Блок except
Блок except запускается, когда блок try не срабатывает из-за исключения. Инструкции в этом блоке часто дают некоторый контекст того, что пошло не так внутри блока try.
Если собираетесь перехватить ошибку как исключение, в блоке except нужно обязательно указать тип этой ошибки. В приведенном выше сниппете место для указания типа ошибки обозначено плейсхолдером <error type> .
except можно использовать и без указания типа ошибки. Но лучше так не делать. При таком подходе не учитывается, что возникающие ошибки могут быть разных типов. То есть вы будете знать, что что-то пошло не так, но что именно произошло, какая была ошибка — вам будет не известно.
При попытке выполнить код внутри блока try также существует вероятность возникновения нескольких ошибок.
Например, вы можете попытаться обратиться к элементу списка по индексу, выходящему за пределы допустимого диапазона, использовать неправильный ключ словаря и попробовать открыть несуществующий файл – и все это внутри одного блока try.
В результате вы можете столкнуться с IndexError, KeyError и FileNotFoundError. В таком случае нужно добавить столько блоков except, сколько ошибок ожидается – по одному для каждого типа ошибки.
Блок else
Блок else запускается только в том случае, если блок try выполняется без ошибок. Это может быть полезно, когда нужно выполнить ещё какие-то действия после успешного выполнения блока try. Например, после успешного открытия файла вы можете прочитать его содержимое.
Блок finally
Блок finally выполняется всегда, независимо от того, что происходит в других блоках. Это полезно, когда вы хотите освободить ресурсы после выполнения определенного блока кода.
Примечание: блоки else и finally не являются обязательными. В большинстве случаев вы можете использовать только блок try, чтобы что-то сделать, и перехватывать ошибки как исключения внутри блока except.
[python_ad_block]
Итак, теперь давайте используем полученные знания для обработки исключений в Python. Приступим!
Обработка ZeroDivisionError
Рассмотрим функцию divide(), показанную ниже. Она принимает два аргумента – num и div – и возвращает частное от операции деления num/div.
def divide(num,div):
return num/div
Вызов функции с разными аргументами возвращает ожидаемый результат:
res = divide(100,8) print(res) # Output # 12.5 res = divide(568,64) print(res) # Output # 8.875
Этот код работает нормально, пока вы не попробуете разделить число на ноль:
divide(27,0)
Вы видите, что программа выдает ошибку ZeroDivisionError:
# Output
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
<ipython-input-19-932ea024ce43> in <module>()
----> 1 divide(27,0)
<ipython-input-1-c98670fd7a12> in divide(num, div)
1 def divide(num,div):
----> 2 return num/div
ZeroDivisionError: division by zero
Можно обработать деление на ноль как исключение, выполнив следующие действия:
- В блоке
tryпоместите вызов функцииdivide(). По сути, вы пытаетесь разделитьnumнаdiv(try в переводе с английского — «пытаться», — прим. перев.). - В блоке
exceptобработайте случай, когдаdivравен 0, как исключение. - В результате этих действий при делении на ноль больше не будет выбрасываться ZeroDivisionError. Вместо этого будет выводиться сообщение, информирующее пользователя, что он попытался делить на ноль.
Вот как все это выглядит в коде:
try:
res = divide(num,div)
print(res)
except ZeroDivisionError:
print("You tried to divide by zero :( ")
При корректных входных данных наш код по-прежнему работает великолепно:
divide(10,2) # Output # 5.0
Когда же пользователь попытается разделить на ноль, он получит уведомление о возникшем исключении. Таким образом, программа завершается корректно и без ошибок.
divide(10,0) # Output # You tried to divide by zero :(
Обработка TypeError
В этом разделе мы разберем, как использовать try и except для обработки TypeError в Python.
Рассмотрим функцию add_10(). Она принимает число в качестве аргумента, прибавляет к нему 10 и возвращает результат этого сложения.
def add_10(num):
return num + 10
Вы можете вызвать функцию add_10() с любым числом, и она будет работать нормально, как показано ниже:
result = add_10(89) print(result) # Output # 99
Теперь попробуйте вызвать функцию add_10(), передав ей в качестве аргумента не число, а строку.
add_10 ("five")
Ваша программа вылетит со следующим сообщением об ошибке:
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-15-9844e949c84e> in <module>()
----> 1 add_10("five")
<ipython-input-13-2e506d74d919> in add_10(num)
1 def add_10(num):
----> 2 return num + 10
TypeError: can only concatenate str (not "int") to str
Сообщение об ошибке TypeError: can only concatenate str (not "int") to str говорит о том, что можно сложить только две строки, а не добавить целое число к строке.
Обработаем TypeError:
- В блок try мы помещаем вызов функции
add_10()с my_num в качестве аргумента. Если аргумент допустимого типа, исключений не возникнет. - В противном случае срабатывает блок
except, в который мы помещаем вывод уведомления для пользователя о том, что аргумент имеет недопустимый тип.
Это показано ниже:
my_num = "five"
try:
result = add_10(my_num)
print(result)
except TypeError:
print("The argument `num` should be a number")
Поскольку теперь вы обработали TypeError как исключение, при передаче невалидного аргумента ошибка не возникает. Вместо нее выводится сообщение, что аргумент имеет недопустимый тип.
The argument `num` should be a number
Обработка IndexError
Если вам приходилось работать со списками или любыми другими итерируемыми объектами, вы, вероятно, сталкивались с IndexError.
Это связано с тем, что часто бывает сложно отслеживать все изменения в итерациях. И вы можете попытаться получить доступ к элементу по невалидному индексу.
В этом примере список my_list состоит из 4 элементов. Допустимые индексы — 0, 1, 2 и 3 и -1, -2, -3, -4, если вы используете отрицательную индексацию.
Поскольку 2 является допустимым индексом, вы видите, что элемент с этим индексом (C++) распечатывается:
my_list = ["Python","C","C++","JavaScript"] print(my_list[2]) # Output # C++
Но если вы попытаетесь получить доступ к элементу по индексу, выходящему за пределы допустимого диапазона, вы столкнетесь с IndexError:
print(my_list[4])
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-7-437bc6501dea> in <module>()
1 my_list = ["Python","C","C++","JavaScript"]
----> 2 print(my_list[4])
IndexError: list index out of range
Теперь вы уже знакомы с шаблоном, и вам не составит труда использовать try и except для обработки данной ошибки.
В приведенном ниже фрагменте кода мы пытаемся получить доступ к элементу по индексу search_idx.
search_idx = 3
try:
print(my_list[search_idx])
except IndexError:
print("Sorry, the list index is out of range")
Здесь search_idx = 3 является допустимым индексом, поэтому в результате выводится соответствующий элемент — JavaScript.
Если search_idx находится за пределами допустимого диапазона индексов, блок except перехватывает IndexError как исключение, и больше нет длинных сообщений об ошибках.
search_idx = 4
try:
print(my_list[search_idx])
except IndexError:
print("Sorry, the list index is out of range")
Вместо этого отображается сообщение о том, что search_idx находится вне допустимого диапазона индексов:
Sorry, the list index is out of range
Обработка KeyError
Вероятно, вы уже сталкивались с KeyError при работе со словарями в Python.
Рассмотрим следующий пример, где у нас есть словарь my_dict.
my_dict ={"key1":"value1","key2":"value2","key3":"value3"}
search_key = "non-existent key"
print(my_dict[search_key])
В словаре my_dict есть 3 пары «ключ-значение»: key1:value1, key2:value2 и key3:value3.
Теперь попытаемся получить доступ к значению, соответствующему несуществующему ключу non-existent key.
Как и ожидалось, мы получим KeyError:
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-2-2a61d404be04> in <module>()
1 my_dict ={"key1":"value1","key2":"value2","key3":"value3"}
2 search_key = "non-existent key"
----> 3 my_dict[search_key]
KeyError: 'non-existent key'
Вы можете обработать KeyError почти так же, как и IndexError.
- Пробуем получить доступ к значению, которое соответствует ключу, определенному
search_key. - Если
search_key— валидный ключ, мы распечатываем соответствующее значение. - Если ключ невалиден и возникает исключение — задействуется блок except, чтобы сообщить об этом пользователю.
Все это можно видеть в следующем коде:
try:
print(my_dict[search_key])
except KeyError:
print("Sorry, that's not a valid key!")
# Output:
# Sorry, that's not a valid key!
Если вы хотите предоставить дополнительный контекст, например имя невалидного ключа, это тоже можно сделать. Возможно, ключ оказался невалидным из-за ошибки в написании. Если вы укажете этот ключ в сообщении, это поможет пользователю исправить опечатку.
Вы можете сделать это, перехватив невалидный ключ как <error_msg> и используя его в сообщении, которое печатается при возникновении исключения:
try:
print(my_dict[search_key])
except KeyError as error_msg:
print(f"Sorry,{error_msg} is not a valid key!")
Обратите внимание, что теперь в сообщении об ошибки указано также и имя несуществующего ключа:
Sorry, 'non-existent key' is not a valid key!
Обработка FileNotFoundError
При работе с файлами в Python часто возникает ошибка FileNotFoundError.
В следующем примере мы попытаемся открыть файл my_file.txt, указав его путь в функции open(). Мы хотим прочитать файл и вывести его содержимое.
Однако мы еще не создали этот файл в указанном месте.
my_file = open("/content/sample_data/my_file.txt")
contents = my_file.read()
print(contents)
Поэтому, попытавшись запустить приведенный выше фрагмент кода, мы получим FileNotFoundError:
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
<ipython-input-4-4873cac1b11a> in <module>()
----> 1 my_file = open("my_file.txt")
FileNotFoundError: [Errno 2] No such file or directory: 'my_file.txt'
А с помощью try и except мы можем сделать следующее:
- Попробуем открыть файл в блоке
try. - Обработаем
FileNotFoundErrorв блокеexcept, сообщив пользователю, что он попытался открыть несуществующий файл. - Если блок
tryзавершается успешно и файл действительно существует, прочтем и распечатаем содержимое. - В блоке
finallyзакроем файл, чтобы не терять ресурсы. Файл будет закрыт независимо от того, что происходило на этапах открытия и чтения.
try:
my_file = open("/content/sample_data/my_file.txt")
except FileNotFoundError:
print(f"Sorry, the file does not exist")
else:
contents = my_file.read()
print(contents)
finally:
my_file.close()
Обратите внимание: мы обработали ошибку как исключение, и программа завершает работу, отображая следующее сообщение:
Sorry, the file does not exist
Теперь рассмотрим случай, когда срабатывает блок else. Файл my_file.txt теперь присутствует по указанному ранее пути.

Вот содержимое этого файла:

Теперь повторный запуск нашего кода работает должным образом.
На этот раз файл my_file.txt присутствует, поэтому запускается блок else и содержимое распечатывается, как показано ниже:

Надеемся, теперь вы поняли, как обрабатывать исключения при работе с файлами.
Заключение
В этом руководстве мы рассмотрели, как обрабатывать исключения в Python с помощью try и except.
Также мы разобрали на примерах, какие типы исключений могут возникать и как при помощи except ловить наиболее распространенные ошибки.
Надеемся, вам понравился этот урок. Успехов в написании кода!
Перевод статьи «Python Try and Except Statements – How to Handle Exceptions in Python».
При выполнении заданий к главам вы скорее всего нередко сталкивались с возникновением различных ошибок. На этой главе мы изучим подход, который позволяет обрабатывать ошибки после их возникновения.
Напишем программу, которая будет считать обратные значения для целых чисел из заданного диапазона и выводить их в одну строку с разделителем «;». Один из вариантов кода для решения этой задачи выглядит так:
print(";".join(str(1 / x) for x in range(int(input()), int(input()) + 1)))
Программа получилась в одну строчку за счёт использования списочных выражений. Однако при вводе диапазона чисел, включающем в себя 0 (например, от -1 до 1), программа выдаст следующую ошибку:
ZeroDivisionError: division by zero
В программе произошла ошибка «деление на ноль». Такая ошибка, возникающая при выполнении программы и останавливающая её работу, называется исключением.
Попробуем в нашей программе избавиться от возникновения исключения деления на ноль. Пусть при попадании 0 в диапазон чисел, обработка не производится и выводится сообщение «Диапазон чисел содержит 0». Для этого нужно проверить до списочного выражения наличие нуля в диапазоне:
interval = range(int(input()), int(input()) + 1)
if 0 in interval:
print("Диапазон чисел содержит 0.")
else:
print(";".join(str(1 / x) for x in interval))
Теперь для диапазона, включающего в себя 0, например, от -2 до 2, исключения ZeroDivisionError не возникнет. Однако при вводе строки, которую невозможно преобразовать в целое число (например, «a»), будет вызвано другое исключение:
ValueError: invalid literal for int() with base 10: 'a'
Произошло исключение ValueError. Для борьбы с этой ошибкой нам придётся проверить, что строка состоит только из цифр. Сделать это нужно до преобразования в число. Тогда наша программа будет выглядеть так:
start = input()
end = input()
# Метод lstrip("-"), удаляющий символы "-" в начале строки, нужен для учёта
# отрицательных чисел, иначе isdigit() вернёт для них False
if not (start.lstrip("-").isdigit() and end.lstrip("-").isdigit()):
print("Необходимо ввести два числа.")
else:
interval = range(int(start), int(end) + 1)
if 0 in interval:
print("Диапазон чисел содержит 0.")
else:
print(";".join(str(1 / x) for x in interval))
Теперь наша программа работает без ошибок и при вводе строк, которые нельзя преобразовать в целое число.
Подход, который был нами применён для предотвращения ошибок, называется «Look Before You Leap» (LBYL), или «посмотри перед прыжком». В программе, реализующей такой подход, проверяются возможные условия возникновения ошибок до исполнения основного кода.
Подход LBYL имеет недостатки. Программу из примера стало сложнее читать из-за вложенного условного оператора. Проверка условия, что строка может быть преобразована в число, выглядит даже сложнее, чем списочное выражение. Вложенный условный оператор не решает поставленную задачу, а только лишь проверяет входные данные на корректность. Легко заметить, что решение основной задачи заняло меньше времени, чем составление условий проверки корректности входных данных.
Существует другой подход для работы с ошибками: «Easier to Ask Forgiveness than Permission» (EAFP) или «проще извиниться, чем спрашивать разрешение». В этом подходе сначала исполняется код, а в случае возникновения ошибок происходит их обработка. Подход EAFP реализован в Python в виде обработки исключений.
Исключения в Python являются классами ошибок. В Python есть много стандартных исключений. Они имеют определённую иерархию за счёт механизма наследования классов. В документации Python версии 3.10.8 приводится следующее дерево иерархии стандартных исключений:
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
| +-- ModuleNotFoundError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
| +-- RecursionError
+-- SyntaxError
| +-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- EncodingWarning
+-- ResourceWarning
Для обработки исключения в Python используется следующий синтаксис:
try:
<код , который может вызвать исключения при выполнении>
except <классисключения_1>:
<код обработки исключения>
except <классисключения_2>:
<код обработки исключения>
...
else:
<код выполняется, если не вызвано исключение в блоке try>
finally:
<код , который выполняется всегда>
Блок try содержит код, в котором нужно обработать исключения, если они возникнут. При возникновении исключения интерпретатор последовательно проверяет в каком из блоков except обрабатывается это исключение. Исключение обрабатывается в первом блоке except, обрабатывающем класс этого исключения или базовый класс возникшего исключения. Необходимо учитывать иерархию исключений для определения порядка их обработки в блоках except. Начинать обработку исключений следует с более узких классов исключений. Если начать с более широкого класса исключения, например, Exception, то всегда при возникновении исключения будет срабатывать первый блок except. Сравните два следующих примера. В первом порядок обработки исключений указан от производных классов к базовым, а во втором – наоборот.
try:
print(1 / int(input()))
except ZeroDivisionError:
print("Ошибка деления на ноль.")
except ValueError:
print("Невозможно преобразовать строку в число.")
except Exception:
print("Неизвестная ошибка.")
При вводе значений «0» и «a» получим ожидаемый соответствующий возникающим исключениям вывод:
Невозможно преобразовать строку в число.
и
Ошибка деления на ноль.
Второй пример:
try:
print(1 / int(input()))
except Exception:
print("Неизвестная ошибка.")
except ZeroDivisionError:
print("Ошибка деления на ноль.")
except ValueError:
print("Невозможно преобразовать строку в число.")
При вводе значений «0» и «a» получим в обоих случаях неинформативный вывод:
Неизвестная ошибка.
Необязательный блок else выполняет код в случае, если в блоке try не вызвано исключение. Добавим блок else в пример для вывода сообщения об успешном выполнении операции:
try:
print(1 / int(input()))
except ZeroDivisionError:
print("Ошибка деления на ноль.")
except ValueError:
print("Невозможно преобразовать строку в число.")
except Exception:
print("Неизвестная ошибка.")
else:
print("Операция выполнена успешно.")
Теперь при вводе корректного значения, например, «5», вывод программы будет следующим:
2.0 Операция выполнена успешно.
Блок finally выполняется всегда, даже если возникло какое-то исключение, не учтённое в блоках except или код в этих блоках сам вызвал какое-либо исключение. Добавим в нашу программу вывод строки «Программа завершена» в конце программы даже при возникновении исключений:
try:
print(1 / int(input()))
except ZeroDivisionError:
print("Ошибка деления на ноль.")
except ValueError:
print("Невозможно преобразовать строку в число.")
except Exception:
print("Неизвестная ошибка.")
else:
print("Операция выполнена успешно.")
finally:
print("Программа завершена.")
Перепишем код, созданный с применением подхода LBYL, для первого примера из этой главы с использованием обработки исключений:
try:
print(";".join(str(1 / x) for x in range(int(input()), int(input()) + 1)))
except ZeroDivisionError:
print("Диапазон чисел содержит 0.")
except ValueError:
print("Необходимо ввести два числа.")
Теперь наша программа читается намного легче. При этом создание кода для обработки исключений не заняло много времени и не потребовало проверки сложных условий.
Исключения можно принудительно вызывать с помощью оператора raise. Этот оператор имеет следующий синтаксис:
raise <класс исключения>(параметры)
В качестве параметра можно, например, передать строку с сообщением об ошибке.
В Python можно создавать свои собственные исключения. Синтаксис создания исключения такой же, как и у создания класса. При создании исключения его необходимо наследовать от какого-либо стандартного класса-исключения.
Напишем программу, которая выводит сумму списка целых чисел, и вызывает исключение, если в списке чисел есть хотя бы одно чётное или отрицательное число. Создадим свои классы исключений:
- NumbersError – базовый класс исключения;
- EvenError – исключение, которое вызывается при наличии хотя бы одного чётного числа;
- NegativeError – исключение, которое вызывается при наличии хотя бы одного отрицательного числа.
class NumbersError(Exception):
pass
class EvenError(NumbersError):
pass
class NegativeError(NumbersError):
pass
def no_even(numbers):
if all(x % 2 != 0 for x in numbers):
return True
raise EvenError("В списке не должно быть чётных чисел")
def no_negative(numbers):
if all(x >= 0 for x in numbers):
return True
raise NegativeError("В списке не должно быть отрицательных чисел")
def main():
print("Введите числа в одну строку через пробел:")
try:
numbers = [int(x) for x in input().split()]
if no_negative(numbers) and no_even(numbers):
print(f"Сумма чисел равна: {sum(numbers)}.")
except NumbersError as e: # обращение к исключению как к объекту
print(f"Произошла ошибка: {e}.")
except Exception as e:
print(f"Произошла непредвиденная ошибка: {e}.")
if __name__ == "__main__":
main()
Обратите внимание: в программе основной код выделен в функцию main. А код вне функций содержит только условный оператор и вызов функции main при выполнении условия __name__ == "__main__". Это условие проверяет, запущен ли файл как самостоятельная программа или импортирован как модуль.
Любая программа, написанная на языке программирования Python может быть импортирована как модуль в другую программу. В идеологии Python импортировать модуль – значит полностью его выполнить. Если основной код модуля содержит вызовы функций, ввод или вывод данных без использования указанного условия __name__ == "__main__", то произойдёт полноценный запуск программы. А это не всегда удобно, если из модуля нужна только отдельная функция или какой-либо класс.
При изучении модуля itertools, мы говорили о том, как импортировать модуль в программу. Покажем ещё раз два способа импорта на примере собственного модуля.
Для импорта модуля из файла, например example_module.py, нужно указать его имя, если он находится в той же папке, что и импортирующая его программа:
import example_module
Если требуется отдельный компонент модуля, например функция или класс, то импорт можно осуществить так:
from example_module import some_function, ExampleClass
Обратите внимание: при втором способе импортированные объекты попадают в пространство имён новой программы. Это означает, что они будут объектами новой программы, и в программе не должно быть других объектов с такими же именами.
Применение try except Python 3 — наиболее распространенный способ обработки ошибок. В этой статье мы познакомимся с лучшими практиками использования try except Python 3.
Используя блок try, можно перехватить исключение и обработать ошибку внутри блока except. Всякий раз, когда код дает сбой внутри блока try, выполнение программы останавливается, и управление передается блоку исключений.
- Для чего используются условия Try-Except Python / Try-Except-else?
- Выведение исключений
- Для чего используются исключения?
- Советы по использованию Try Except Python 3, Try-Except-Else и многое другое
- Как обрабатывать произвольное исключение
- Перехват нескольких исключений в одном блоке except
- Обработка нескольких исключений в одном блоке except
- Повторное выведение исключений в Python
- Когда использовать условие else
- Использование блока [finally]
- Используйте ключевое слово as для перехвата конкретных типов исключений
- Лучшие практики вывода исключений
- Как пропустить ошибки и продолжить выполнение кода
- Как использовать Try-Except в Python
С помощью try-except и try-except-else можно избежать многих проблем, которые могут возникнуть. Например, код Python, использующий стиль LBYL (Look before you leap), может привести к зацикливанию условий. В подобном случае поможет try-except.
Также бывают случаи, когда правильность выполнения кода зависит от актуальности данных. Например, код, вызывающий os.path.exists или Queue.full, может завершиться неудачно. Эти функции могут возвращать данные, которые устаревают к моменту их использования. Поэтому лучше использовать try-except-else.
В Python можно выводить исключение каждый раз, когда это необходимо. Это можно сделать, вызвав из кода [raise Exception (‘Test error!’)]. После чего исключение остановит текущее выполнение и перейдет дальше по стеку вызовов до тех пор, пока исключение не будет обработано.
Исключения также используются для управления ошибками, которые возникают в циклах, при работе с базой данных, доступе к сети и т.д.
Обработка исключений — это искусство, которое помогает в написании надежного и качественного кода.

Как в Python лучше всего использовать Try-Except
Иногда может понадобиться способ для обработки и вывода сообщения об ошибке. Это можно сделать с помощью исключений Python. Во время тестирования вы можете разместить код внутри блока try.
try:
#ваш код
except Exception as ex:
print(ex)
Можно перехватить несколько исключений в одном блоке except.
except (Exception1, Exception2) as e:
pass
Обратите внимание, что в Python 2.6 / 2.7 нужно отделить исключения от переменной запятой. Но этого нельзя делать в Python 3. Вместо этого необходимо использовать ключевое слово [as].
Существует множество способов обработки сразу нескольких исключений. Первый из них требует размещения всех исключений, которые могут возникнуть. Смотрите приведенный ниже код:
try:
file = open('input-file', 'open mode')
except EOFError as ex:
print("Caught the EOF error.")
raise ex
except IOError as e:
print("Caught the I/O error.")
raise ex
Последний метод — использовать исключение без упоминания какого-либо атрибута исключения.
try:
file = open('input-file', 'open mode')
except:
# In case of any unhandled error, throw it away
raise
Последний вариант может быть полезен, если у вас нет информации об исключении, которое может возникнуть.
Однажды выведенные исключения продолжают обрабатываться в вызывающих их методах до тех пор, пока они не будут обработаны. Но можно добавить условие except, которое содержит только вызов [raise] без каких-либо аргументов. Это приведет к повторному исключению.
Смотрите приведенный ниже код.
try:
# Намеренное выведение исключения.
raise Exception('I learn Python!')
except:
print("Entered in except.")
# Повторное выведение исключения.
raise
Вывод:
Entered in except. Traceback (most recent call last): File "python", line 3, in <module> Exception: I learn Python!
Используйте условие else после блока try-except. Оно будет выполняться в том случае, если исключение не будет выдано. Оператор else всегда должен предшествовать блокам except .
В блоках else можно добавить код, который необходимо запустить, если ошибок не возникло.
В приведенном ниже примере видно, что цикл while работает бесконечно. Код запрашивает значение у пользователя, а затем анализирует его с помощью встроенной функции [int()]. Если пользователь вводит нулевое значение, блок except будет заблокирован. В противном случае код будет проходить через блок else.
while True:
# Введете с консоли целое число.
x = int(input())
# Разделите 1 на x, чтобы протестировать ошибку
try:
result = 1 / x
except:
print("Error case")
exit(0)
else:
print("Pass case")
exit(1)
Если есть код, который должен выполняться во всех случаях, разместите его внутри блока [finally]. Python всегда будет запускать эти инструкции. Это самый распространенный способ выполнения задач очистки. Вы также можете проверить это.
Ошибка перехватывается условием try. После того, как будет выполнен код в блоке except, выполняется инструкции в [finally].
Смотрите приведенный ниже код.
try:
# Намеренно выводим ошибку.
x = 1 / 0
except:
# Условие Except:
print("Error occurred")
finally:
# Условие Finally:
print("The [finally clause] is hit")
Вывод:
Error occurred The [finally clause] is hit
С помощью <identifier> можно создать объект исключения. В приведенном ниже примере мы создаем объект IOError, а затем используем его внутри условия.
try:
# Намеренно выводим ошибку.
f = open("no-file")
except IOError as err:
# Создаем экземпляр IOError для учета.
print("Error:", err)
print("Code:", err.errno)
Вывод:
('Error:', IOError(2, 'No such file or directory'))
('Code:', 2)
Избегайте создания общих исключений, иначе конкретные exception также будут перехватываться. Лучшей практикой является отображение конкретного исключения, близкого к возникшей проблеме.
Не рекомендуется:
def bad_exception():
try:
raise ValueError('Intentional - do not want this to get caught')
raise Exception('Exception to be handled')
except Exception as error:
print('Inside the except block: ' + repr(error))
bad_exception()
Вывод:
Inside the except block: ValueError('Intentional - do not want this to get caught',)
Рекомендуется:
В приведенном ниже примере перехватывается конкретный тип исключения, а не общий. Мы также используем параметр args для вывода некорректных аргументов, если они есть. Рассмотрим этот пример.
try:
raise ValueError('Testing exceptions: The input is in incorrect order', 'one', 'two', 'four')
except ValueError as err:
print(err.args)
Вывод:
('Testing exceptions: The input is in incorrect order', 'one', 'two', 'four')
Лучше не применять данную практику. Но если это нужно, то используйте следующий пример.
try:
assert False
except AssertionError:
pass
print('Welcome to Prometheus!!!')
Вывод:
Рассмотрим наиболее распространенные исключения в Python с примерами.
Распространенные ошибки исключений:
- IOError–возникает, если файл не открывается.
- ImportError — если модуль python не может быть загружен или размещен.
- ValueError — возникает, если функция получает аргумент корректного типа, но с некорректным значением.
- KeyboardInterrupt — когда пользователь прерывает выполнение кода нажатием на Delили Ctrl-C.
- EOFError — когда функции input() / raw_input()достигают конца файла (EOF), но без чтения каких-либо данных.
Примеры распространенных исключений
except IOError:
print('Error occurred while opening the file.')
except ValueError:
print('Non-numeric input detected.')
except ImportError:
print('Unable to locate the module.')
except EOFError:
print('Identified EOF error.')
except KeyboardInterrupt:
print('Wrong keyboard input.')
except:
print('An error occurred.')
В процессе программирования ошибки неизбежны. Но их можно обработать, используя конструкции try-except или try-except-else, try-except-finally.
Можно писать программы, которые обрабатывают выбранные исключения. Посмотрите на следующий пример, который запрашивает ввод у пользователя до тех пор, пока не будет введено правильное целое число, но позволяет пользователю прерывать программу, используя Ctrl-C или что-либо поддерживаемое операционной системой. Обратите внимание, что сгенерированное пользователем прерывание сигнализируется возбуждением исключения KeyboardInterrupt.
while True: try: x = int(input("Пожалуйста, введите целое число: ")) break except ValueError: print("Это не целое число. Попробуйте снова...")
try/except работает следующим образом
:
- Сначала выполняется инструкция
try— код между ключевыми словамиtryиexcept. - Если исключение не возникает, инструкция
exceptпропускается и выполнение оператораtryзавершается. - Если во время выполнения кода в инструкции
tryвозникает исключение, остальная часть кода этого блока пропускается. Затем, если тип исключения соответствует исключению, записанному после ключевого словаexcept, блок кода этой инструкцииexceptвыполняется, а затем выполнение программы продолжается после всей конструкцииtry. - Если возникает исключение, которое не соответствует исключению, записанному в инструкции
except, оно передается внешним операторамtry. Если обработчик не найден, то это считается необработанным исключением, следовательно выполнение останавливается с сообщением об ошибке выполнения.
Оператор try может содержать несколько инструкций except, чтобы указать обработчики для различных исключений. В этом случае будет выполнен только один обработчик. Обработчики обрабатывают исключения, возникающие только в соответствующей конструкции try, а не в других обработчиках, например вложенных, того же оператора try. Инструкция except может иметь несколько исключений в виде кортежа, заключенного в скобки, например:
except (RuntimeError, TypeError, NameError): pass
Класс в инструкции except совместим с исключением, если это тот же самый класс или его базовый класс, но не наоборот. Инструкция except, перечисляющая производный класс, не совместима с базовым классом. Например, следующий код будет печатать B, C, D в таком порядке:
class B(Exception): pass class C(B): pass class D(C): pass for cls in [B, C, D]: try: raise cls() except D: print('D') except C: print('C') except B: print('B')
Обратите внимание, что если бы инструкции except были отменены (с первым исключением B), то вывелось бы B, B, B — срабатывает первое совпадающее предложение except.
Последняя инструкция except может опустить имя исключения. Используйте это с крайней осторожностью, таким образом можно легко замаскировать реальную ошибку программирования! Здесь можно использовать sys.exc_info() или модуль traceback для вывода сообщения об ошибке или ее сохранения для дельнейшего анализа, а затем повторно вызвать исключение при помощи оператора raise, что позволяет вызывающей стороне также обработать исключение:
import traceback try: f = open('myfile.txt') s = f.readline() i = int(s.strip()) except OSError as err: print("OS error: {0}".format(err)) except ValueError: print("Не удалось преобразовать данные в целое число.") except: print("Непредвиденная ошибка...") # сохраняем исключение для дальнейшего анализа. with open('trace.txt', 'a') as fp: traceback.print_exc(file=fp) # повторный вызов исключения, если это необходимо. raise
В примере выше, сведения об ошибке сохраняются в файл trace.txt с использованием встроенной функции open(). Лучшей практикой сохранения исключений для дальнейшего анализа в подобных ситуациях является применение модуля logging.
Конструкция try/except может содержать необязательную инструкцию else, которая при наличии должна следовать за всеми инструкциями except. Блок кода в инструкции else будет выполнен в том случае, если код в инструкции try не вызывает исключения. Например:
for arg in sys.argv[1:]: try: f = open(arg, 'r') except OSError: print('не удается открыть', arg) else: print(arg, 'имеет', len(f.readlines()), 'строк') f.close()
Использование инструкции else лучше, чем добавление дополнительного кода в инструкцию try, поскольку позволяет избежать случайного перехвата исключения, которое не было вызвано кодом, защищенным конструкцией try/except.
Когда возникает исключение, оно может иметь связанное значение, также известное как аргумент исключения. Наличие и тип аргумента зависят от типа исключения.
Инструкция except может указывать переменную после имени исключения. Переменная привязывается к экземпляру исключения с аргументами, хранящимися в экземпляре instance.args. Для удобства, экземпляр исключения определяет __str__(), так что аргументы могут быть напечатаны непосредственно без необходимости ссылаться instance.args. Кроме того, можно создать экземпляр исключения прежде, чем вызвать его и добавить любые атрибуты к нему по желанию.
try: raise Exception('spam', 'eggs') except Exception as inst: # экземпляр исключения print(type(inst)) # аргументы, хранящиеся внутри print(inst.args) # __str__ позволяет печатать args напрямую, но может # быть переопределен в подклассах исключений print(inst) # распаковка аргументов x, y = inst.args print('x =', x) print('y =', y) # <class 'Exception'> # ('spam', 'eggs') # ('spam', 'eggs') # x = spam # y = eggs
Если исключение имеет аргументы, они печатаются как последняя часть («деталь») сообщения для необработанных исключений.
Обработчики исключений обрабатывают не только исключения возникающие непосредственно в предложении try, но также если они возникают внутри функций, которые вызываются, даже косвенно, в коде оператора try. Например:
def this_fails(): x = 1/0 try: this_fails() except ZeroDivisionError as err: print('Handling run-time error:', err) #Handling run-time error: division by zero
Перевод статьи Learning (not) to Handle Exceptions
В этой статье мы изучим основы обработки ошибок выполнения вашего кода в Python, а также научимся использовать свои собственные (пользовательские) исключения. Вы узнаете, почему иногда лучше не перехватывать исключения и как разработать свой шаблон написания кода, с которым будет удобно работать другим пользователям. Исключения являются важной частью любой программы Python, и их элегантное и эффективное применение на практике значительно повысит ценность и полезность вашего кода.
Примечание переводчика. Многие примеры кода, которые используется в этой статье имеют цель продемонстрировать особенности использования механизма исключений в языке Python. Иногда для наглядности представления особенностей использования инструкций кода, в ущерб практической направленности, некоторые примеры кода довольно надуманы.
Как всегда, вы можете самостоятельно ознакомиться с примерами кода для этой статьи, а также её исходным текстом, и если у вас есть предложения по его улучшению, пишите.
Содержание
- Инструкции Try/Except
- Перехват исключений заданного типа
- Повторное возбуждение исключений
- Использование исключений в исключениях
- Обработка нескольких исключений
- Блок finally
- Блок else
- Работа с traceback (трассировка)
- Применение пользовательских исключений
- Примеры хороших практик использования пользовательских исключений
- Добавляем к исключениям аргументы
- Выводы
Инструкции Try/Except
Допустим, что вы хотите прочитать из файла некоторые данные. Для решения этой задачи вы можете написать что-то вроде этого:
f = open('my_file.dat')
data = f.readfile()
print('Data loaded')
И если вы выполните этот код и файла с именем my_file.dat в рабочей директории нет, то увидите следующее сообщение:
FileNotFoundError: [Errno 2] No such file or directory: 'my_file.dat'
То есть такого файла или директории нет и после появления ошибки код вашей программы не будет выполнен полностью — произойдет её аварийное завершение. Вот почему вы не увидите результата выполнения последней инструкции: сообщения Data loaded в консоли. Это простой пример, часто возникающих на практике ошибок.
Теперь, представьте, что ваша программа в ходе выполнения обращается к некоторому устройству. И если в вашей программе возникает ошибка, то у вас не будет возможности корректно закрыть соединение, чтобы надлежащим образом освободить ресурсы с которыми работало ваше приложение. Это может привести как к потере данных, так и выводу из строя оконечного устройства, которым она управляла.
Работа с такого рода ошибками обычно выполняется с использованием блока инструкций try/except . Из официальной документации известно, что если внутри блока try в коде возникает ошибка, то будет выполнен код в блоке except. С учетом сказанного перепишем код из примера выше следующим образом:
try:
f = open('my_file.dat')
f.readfile()
print('Loaded data')
except:
print('Data not loaded')
Если теперь мы его запустим на выполнение, то увидим на экране сообщение: Data not loaded (Данные не загружены). И это замечательно! Теперь наша программа аварийно не завершает свою работу, и мы можем корректно закрыть соединение или прервать связь с нашим устройством. Однако мы всё ещё не знаем причину, по которой данные не были загружены.
Прежде чем продолжить, создадим пустой файл с именем my_file.dat в рабочей директории и снова запустим наш скрипт на исполнение. И увидим, что данные всё же не загружаются, независимо от того, существует файл или нет, так как наш файл пуст. Таким образом при разработке вашего проекта множество неопределенностей и неоднозначных ситуаций, которые возникают в коде блока except даже в таком простом примере. То есть мы не знаем почему данные не загружены: либо файла нет, либо же он просто пуст, ничего не содержит. Давайте рассмотрим следующий пример кода:
open('my_file.dat')
f.readfile()
print('Loaded data')
После его выполнения получим в консоли ошибку вида:
AttributeError: '_io.TextIOWrapper' object has no attribute 'readfile'
Сообщение об ошибке информирует нас о том, что проблема заключается в том методе readfile, который мы попытались использовать, и что его не существует в полученном объекте.
Если вы используете простой блок try/except , вы будете обрабатывать абсолютно все возможные исключения, но на самом деле у нас нет возможности узнать, что всё же пошло не так, то есть причину появления ошибки. В простых случаях, таких как приведен выше, у вас есть только две строчки кода, которые можно легко проанализировать и найти причину ошибки. Однако, если вы разрабатываете свой пакет или сложную функцию с большим числом вложений кода, то ошибки будут распространяться далее по ходу потока исполнения кода, и вы не знаете, как они повлияют на работу остальной части программы и что именно вызвало их появление.
Чтобы вы имели представление о важности правильной обработки ошибок, я приведу вам пример, ситуации, возникшей при эксплуатации прикладного приложения, которое поставлялось вместе со сложным лабораторным оборудованием. Эта программа управляет прибором Nano Sight и имеет очень приятный пользовательский интерфейс. Однако при сохранении данных, если выбранное имя файла содержало точку, данные не сохранялись. К сожалению, в этом случае данные безвозвратно терялись, и пользователь никогда не узнал бы, что причина возникновения проблемы на самом деле проста: наличие символа . в имени файла.
Поэтому разработка изящного способа обработки всех возможных ошибок очень сложна, а иногда практически невозможна. Тем не менее, вы можете видеть, что программное обеспечение, тем более поставляемое с очень дорогим оборудованием (в данном случае я имею в виду инструменты с ценой, как у небольшой квартиры), должно учитывать возникновение всевозможных видов ошибок и корректную обработку возбужденных исключений.
Перехват исключений заданного типа
Самый правильный способ обработки исключений в Python — указать, какой тип исключения мы ожидаем и соответствующим образом его обработать . Таким образом, если мы узнаём, что проблема в том, что файла не существует, то мы можем его создать. Если же проблема имеет другую природу, то будет генерироваться сообщение с информацией об исключении и отображаться для информирования пользователя. Давайте изменим пример, приведенный выше, следующим образом:
try:
file = open('my_file.dat')
data = file.readfile()
print('Data Loaded')
except FileNotFoundError:
print('This file doesn't exist')
Если вы запустите этот скрипт, а файла my_file.dat в рабочей директории нет, то он выведет на экран сообщение, что файл не существует, и программа продолжит работу. Однако, если файл существует, вы увидите исключение связанное с отсутствием у объекта file метода readfile (такого метода конечно же не существует — это преднамеренная ошибка).
На самом деле мы можем ограничиваться простой печатью сообщения, если происходит перехват исключения — в случае отсутствия файла его легко создать:
try:
file = open('my_file.dat')
data = file.readfile()
print('Data Loaded')
except FileNotFoundError:
file = open('my_file.dat', 'w')
print('File created')
file.close()
Если вы запустите этот скрипт, то увидите, что теперь файл my_file.dat будет создан. Если вы запустите скрипт во второй раз, то будет возбуждено исключение из-за отсутствия метода readfile.
Теперь представьте, что вы не указываете, какое конкретно исключение вы собираетесь перехватывать, и у вас есть следующий код, что произойдет, когда вы его запустите?
try:
file = open('my_file.dat')
data = file.readfile()
print('Data Loaded')
except:
file = open('my_file.dat', 'w')
print('File created')
Если вы его внимательно проанализируете, то поймете, что, даже если файла my_file.dat существует, будет возбуждено исключение, ввиду отсутствия метода readfile. Затем будет выполнен код в блоке except. В этом блоке наша программа создаст новый файл my_file.dat, даже если он уже существует, и, следовательно, вы потеряете все информацию, которая в нем до этого хранилась. Что же мы можем сделать для того, что бы избежать этого рассмотрим далее.
Повторное возбуждение исключений
На практике распространен следующий приём написания кода, когда сначала возбуждается некоторое исключение, далее вы выполняете определённые действия, а затем снова инициализируется это же исключение. Этот прием используется, например, если вы осуществляете запись информации в базу данных или в файл. Представим себе следующую ситуацию: вы храните информацию в двух файлах: в первом вы храните значения частотного спектра, а во втором — температуру, при этом записи в отдельных файлах записываются по порядку и их соответствующие значения снимаются с датчиков в один и тот же момент времени. Далее вы сначала сохраняете в один файл значения спектра, а затем значение температуры уже в другой файл. То есть каждая строка в любом из файлов соответствует в прямом порядке следования некоторой другой строке в другом.
Предположим, что сначала мы сохраняем значения спектра, а затем температуры. Однако время от времени, когда вы пытаетесь считывать показания датчиков у прибора, он сбоит, и значение датчика температуры не считывается. Таким образом, если в этот цикл записи мы не сохраним значение температуры, у вас образуется несоответствие в порядке данных по времени их съёма, потому что во втором файле будет отсутствует строка данных, соответствующая показаниям датчика температуры. В то же время вам не нужно, чтобы эксперимент продолжался далее, так как датчик температуры не исправен и вам необходимо провести с ним некоторые манипуляции. Для решения этой задачи вы можете использовать такой пример кода:
#Данные сохранены. Начало нового цикла опроса датчиков
try:
temp = instrument.readtemp() # пытаемся опросить датчик температуры
except:
remove_last_line(data_file)
# если датчик не отвечает то удалим последнюю запись в файле, записанную в текущем цикле
raise
# возбудим это исключение снова для того, чтобы пользователь был оповещен о возникшей ошибке
save_temperature(temp)
# если все нормально сохраняем показания датчика в файл
Таким образом сначала мы пытаемся получить данные от датчика температуры, и если что-то пойдет не так, мы перехватываем возбужденное исключение, инициированное из-за того, что датчик нам не отвечает. Затем мы удаляем последнюю строку из нашего первого файла с данными о спектре, а затем снова инициируем исключение с помощью оператора raise. Эта команда повторно возбуждает исключение того же типа, что было перехвачено в блоке except. Благодаря этой стратегии мы будем уверены в том, что наши данные в обоих файлах будут согласованны (то есть имеют одинаковую длину или количество записей). А также в том, что программа далее не будет продолжать работать в том, же режиме (и опрашивать нерабочие датчики), и пользователь увидит сообщение с всей необходимой информации о том, что же пошло не так (тип ошибки).
Существует еще один способ практического применения повторного возбуждения исключений. И если кто-то, из читающих эту статью, знает другие способы использования этого приема, напишите мне и я обновлю содержимое статьи с учетом ваших дополнений.
Дополнение. Дело в том, что инструкция except, из примера кода выше, осуществляет перехват абсолютно всех типов исключений, возникающих в ходе выполнения программы. Если же справа от нее указать название класса перехватываемого исключения (или кортеж в котором они перечислены), то этот блок кода будет выполнен лишь при перехвате исключения соответствующего класса.
Рассмотрим следующий пример кода.
try:
# код, который мы проверяем
except:
# при перехвате любого типа исключений
# этот блок кода будет выполнен
raise
except Exception_classname1:
# при перехвате исключения с классом Exception_name1
# этот блок кода будет выполнен
except Exception_classname2:
# при перехвате исключения с классом Exception_name2
# этот блок кода будет выполнен
Как видно первый блок except будет перехватывать абсолютно все типы исключений и соответственно при возникновении любой исключительной ситуации код внутри этого блока будет выполнен всегда. Следующие блоки except будут выполняться только при перехвате исключений соответствующих классов Exception_classname1 и Exception_classname2 .
А значит в первый блок except можно поместить код, который необходимо выполнять при возникновении исключительной ситуации любого вида. После чего инициируем возбуждение исключения того же типа с помощью инструкции raise.
Следующие блоки except с соответствующими классами будут перехватывать заданные типы исключений и внутри их блоков можно добавить код, который будет выполняться в зависимости от вида возникшей исключительной ситуации. Не правда ли, что это очень удобно.
С синтаксисом использования инструкции except вы можете ознакомиться по ссылке. С классами исключительных ситуаций по этой ссылке.
Использование исключений в исключениях
Теперь представим, что наш код является частью функции, отвечающей за открытие файла, загрузку его содержимого или создание нового файла в случае, если файла с указанным именем не существует. Сценарий будет выглядеть так же, как и ранее, с той разницей, что имя файла будет задаваться в переменной:
try:
file = open(filename)
data = file.readfile()
except FileNotFoundError:
file = open(filename, 'w')
Чтобы запустить приведенный этот скрипт на выполнение, мы должны указать имя файла:
filename = 'my_data.dat'
try :
[ ... ]
Если вы запустите этот код, то заметите, что он ведет себя точно так, как ожидалось. Однако, если вы укажете пустое имя файла:
filename = ''
try:
[...]
Вы увидите выведенное на экран сообщение с большим количеством информации о возникшей ошибке, содержащей лишь одну важную строку:
During handling of the above exception, another exception occurred:
Если вы её проанализируете, то увидите, что во время обработки вышеупомянутого исключения было возбуждено другое исключение. К сожалению, это обычная ситуация, особенно когда речь идет о пользовательском вводе.
Решить возникшую причину можно, вложив в наш блок except еще один блок инструкций try/except или же предварительно проверять валидность значения переменной filename перед вызовом инструкции open. Но самый правильный способ избежать возникновения этой ситуации мы рассмотрим ниже.
Обработка нескольких исключений
До сих пор мы рассматривали случаи возбуждения только одного типа исключений, например FileNotFoundError. Тем не менее, листинг кода, который мы уже рассматривали выше, вызывает появление двух исключений различных типов, а точнее FileNotFoundError и AttributeError. Рассмотрим приемы работы с несколькими типами исключений на практике.
Конечно же вы можете специально создавать ситуации, приводящие к возбуждению исключений. Например, если вы запустите на выполнение следующий код:
file = open('my_data.dat', 'a')
file.readfile()
То получите сообщение:
AttributeError: '_io.TextIOWrapper' object has no attribute 'readfile'
Первая часть полученного сообщения (а точнее до символа :) — это имя класса возбужденного исключения, то есть AttributeError, а вот вторая часть — это информация о причине, вызвавшей инициализацию полученного исключения. Отметим, что исключения одного типа могут иметь разное содержимое второй части, сообщения об ошибке, а значит и причины инициализации исключения, а следовательно подробнее описывают то, что его вызвало.
Для решения нашей задачи необходимо перехватить исключения 2 типов: AttributeError и FileNotFound. Поэтому наш код будет иметь вид:
filename = 'my_data.dat'
try:
file = open(filename)
data = file.readfile()
except FileNotFoundError:
file = open(filename, 'w')
print('Created file')
except AttributeError:
print('Attribute Error')
Теперь мы знаем как можно обрабатывать в программе несколько типов исключений. Отметим, что если в какой-либо строке кода внутри блока try возникает исключение, то остальная часть кода в этом блоке не будет выполнена. То есть если файла my_data.dat не существует то, следующая строка кода, то есть data = file.readfile() не будет выполняться.
Далее поток выполнения интерпретатора Python будет последовательно обходить все except. И если будет найдено соответствие типа возбужденного исключения с классом исключений справа от каждой инструкции except, то код внутри этого блока будет выполнен. Поэтому всегда единовременно будет возбуждено только одно исключение определенного типа.
В нашем же случае при первом запуске кода файла my_data.dat не существует, то есть будет инициализироваться исключение типа — FileNotFoundError. Файл будет создан и в консоли мы получим сообщение Created file. При повторном запуске нашего кода будет возбуждено исключение класса AttributeError. Так файл существует, но созданный объект file не имеет метода readfile(), то попытка его вызвать завершится возбуждением соответствующего исключения.
Отметим, что мы можем добавить в конце нашего кода еще один блок except без наименования класса исключения справа для того, чтобы перехватывать все другие исключения, возникающие в ходе выполнения нашей программы:
filename = 'my_data.dat'
try:
file = open(filename)
data = file.read()
important_data = data[0]
except FileNotFoundError:
file = open(filename, 'w')
print('Created file')
except AttributeError:
print('Attribute Error')
except:
print('Unhandled exception')
В случае если файл my_data.dat существует, но он пуст, у нас возникнет еще одна проблема при попытке доступа к содержимому data[0] (получить первый символ строки, в которую мы прочитали содержимое файла). Мы не готовы к обработке этого исключения и поэтому напечатаем лишь сообщение о необработанном исключении (Unhandled exception). Тем не менее, было бы более полезно сообщать пользователю, какое же исключение было возбуждено. Для этого мы можем использовать следующий приём:
filename = 'my_data.dat'
try:
file = open(filename)
data = file.read()
important_data = data[0]
except Exception as e:
print('Unhandled exception')
print(e)
Результатом выполнения этого кода будет следующее сообщение:
Unhandled exception string index out of range
Так как исключения имеют свой определённый тип type, соответствующий наименованию предопределенных классов исключений (официальная документация), то его также можно использовать. Например, следующим образом:
filename = 'my_data.dat'
try:
file = open(filename)
data = file.read()
important_data = data[0]
except Exception as e:
print('Unhandled exception')
if isinstance(e, IndexError):
print(e)
data = 'Information'
important_data = data[0]
print(important_data)
После запуска этого кода на выполнение, он напечатает в консоли первую букву строки Information, то есть I.
Отметим, что изучение этого примера кода имеет своей целью продемонстрировать возможности использования информации о возбужденном исключении, в ущерб практической значимости. Как мы можем видеть, его главный недостаток заключается в том, что переменная important_data будет в конечном итоге не определена. Это произойдет в случае если файл my_data.dat не существует. И поэтому мы получим соответствующее сообщение об ошибке:
NameError: name 'important_data' is not defined
Чтобы не допустить таких ситуаций мы можем добавить еще один блок в последовательность блоков-перехватчиков except, это блок с инструкцией finally.
Блок finally
Код в блоке с инструкцией finally будет выполняться всегда независимо от того, возбуждались ли исключения в блоке try или нет.
filename = 'my_data.dat'
try:
file = open(filename)
data = file.read()
important_data = data[0]
except Exception as e:
if isinstance(e, IndexError):
print(e)
data = 'Information'
important_data = data[0]
else:
print('Unhandled exception')
finally:
important_data = 'A'
print(important_data)
Приведенный выше пример кода очень прост, но совершенно не применим на практике, так как мы специально устанавливаем значение переменной important_data. Тем не менее он наглядно поясняет принцип использования оператора finally . Таким образом если необходимо в коде производить какие либо действия с данными или внешними устройствами, и вы хотите быть абсолютно уверены, что они будут выполнены в любом случае (при возникновении любых ошибок или сбоев), то вы можете включить код этих операций в блок инструкции finally.
Используя блок finally можно быть уверенным, что вы закроете соединение с устройством или файл, открытый для записи или чтения и т. д. Или в общем говоря, корректно освободите все ресурсы, задействованные при работе с вашим программным обеспечением.
И наконец, поведение блока finally, в частности и в нашем примере, будет разным, в зависимости от того сколько раз мы его запустим на исполнение. Давайте рассмотрим следующий пример:
filename = 'my_data.dat'
try:
print('In the try block')
file = open(filename)
data = file.read()
important_data = data[0]
except FileNotFoundError:
print('File not found, creating one')
file = open(filename, 'w')
finally:
print('Finally, closing the file')
file.close()
important_data = 'A'
print(important_data)
Сначала запустим код, для ситуации когда файла my_data.dat не существует. И увидим следующий результат его выполнения:
In the try block File not found, creating one Finally, closing the file
Итак, мы видим , что код последовательно выполнялся в блоках: сначала в try, затем во всех блоках except и наконец в finally. Если вы снова запустите код, то файл уже будет существовать, и, следовательно, результат его выполнения будет совершенно другим:
In the try block
Finally, closing the file
Traceback (most recent call last):
File "JJ_exceptions.py", line 7, in
important_data = data[0]
IndexError: string index out of range
Из полученного сообщения мы видим, что когда возникает необработанное исключение, следующий блок, который будет выполнен, — это finally . Поэтому у нас появляется возможность корректно закрыть наш файл до аварийного завершения программы (освободить ресурсы).
Этот подход удобен тем, что предотвращает любой конфликт с кодом ниже. Вы открываете, а затем при возникновении ошибки закрываете файл, при этом остальная часть нашей программы решает проблему обработки исключения типа IndexError. Если вы хотите запустить наш пример без инициализации исключений, просто напишите что либо в файл my_data.dat, открыв его в простом текстовом редакторе, и вы увидите результат выполнения нашей программы без ошибок и соответственно возбуждения исключений.
Блок else
Изучая вопросы обработки и инициализации исключений, необходимо рассмотреть еще одну инструкцию, которую можно использовать совместно с уже рассмотренными — это инструкция else . Основная идея её использования заключается в том, что код внутри её блока выполняется, если в блоке try не возникло ошибок, значит не инициировалось возбуждение исключений . Из следующего примера, можно очень легко понять как это работает:
filename = 'my_data.dat'
try:
file = open(filename)
except FileNotFoundError:
print('File not found, creating one')
file = open(filename, 'w')
else:
data = file.read()
important_data = data[0]
Самое сложное — это понять полезность использования блока else . И в принципе, код, который мы включили в блок else, мог быть помещен сразу после строки кода, в которой мы открываем файл, как мы это делали ранее. Тем не менее, мы можем использовать блок else для предотвращения перехвата исключений, которые не относятся к коду в блоке try .
Не понятно? Тогда рассмотрим следующий немножко надуманный, но наглядный пример кода. Представьте, что вам необходимо прочитать имя файла из другого файла, а затем открыть его для чтения/записи. Наш код для решения этой задачи будет выглядеть следующим образом:
try:
file = open(filename)
new_filename = file.readline()
except FileNotFoundError:
print('File not found, creating one')
file = open(filename, 'w')
else:
new_file = open(new_filename)
data = new_file.read()
Поскольку мы открываем по очереди два файла, то вполне возможно, что проблема будет в том, что второго файла с указанным именем не существует. Если бы мы поместили этот код в блок try, то мы в конечном итоге в блоке except перехватим исключение, инициированное из-за ошибки открытия второго файла, даже если нам нужно перехватить ошибку открытия первого. Конечно этот пример может быть не совсем правдоподобен, но он наглядно поясняет суть использования инструкции else.
Теперь объединим всё, что мы узнали о приемах обработки и инициализации исключений вместе:
try:
file = open(filename)
new_filename = file.readline()
except FileNotFoundError:
print('File not found, creating one')
file = open(filename, 'w')
else:
new_file = open(new_filename)
data = new_file.read()
finally:
file.close()
Проаназируйте его самостоятельно. Настоятельно рекомендую позже поиграть с примерами кода, представленными в этой статье и посмотреть на практике работу рассмотренных приёмов использования каждого блока инструкций. Если вы еще не достаточно хорошо знаете Python, то возможно, вы столкнетесь с чем то для вас непонятным, и это будет связано с инициализацией и обработкой исключений. Это заставит вас заново запускать скрипты и анализировать полученные результаты снова и снова.
Теперь вы знаете, что использование механизма инициализации и обработки исключений, может применяться на практике разными способами. Отличным ресурсом, содержащим все те материалы, что мы с вами рассмотрели в этой статье, является, конечно же, официальная документация использования исключений в Python.
Работа с traceback (трассировка)
Как вы, наверное, уже могли видеть, когда инициализируется исключение, то на экран консоли выводится большое количество различной информации, описывающей что же все-таки произошло. Например, если вы попытаетесь открыть несуществующий файл, то вы получите следующее сообщение:
Traceback (most recent call last):
File "P_traceback.py", line 13, in
file = open(filename)
FileNotFoundError: [Errno 2] No such file or directory: 'my_data.dat'
Интерпретация получаемых после возбуждения исключений сообщений требует наличия некоторой практики, но для простых случаев это достаточно легко сделать даже начинающим.
Во-первых, в сообщении приведен traceback, или простыми словами, история всех действий интерпретатора, результат выполнения которых привёл к инициализации исключения. И конечно сам процесс детальной расшифровка traceback требует написания отдельного поста.
Во-вторых из нашего примера сообщения сразу можно определить путь к файлу, который стал причиной возникшей ошибки, а также номер строки исходного кода скрипта, выполнение которой привело к ошибке. Если затем вы откроете файл исходного кода в текстовом редакторе и перейдете к этой строке, то увидите, что это строка, содержащую следующие инструкции: file = open(filename). И наконец из сообщения, вы можете узнать тип возникшего исключения FileNotFoundError, также как мы это делали ранее.
В последнем примере с выведенным на экран сообщением о возбужденном исключения мы пренебрегали использованием модуля traceback из состава стандартной библиотеки Python. Что позволило бы нам более точно определить источник ошибок, приведший к возбуждению исключений. К счастью, Python и возможности его стандартной библиотеки позволяют легко получить доступ к информации traceback и осуществлять трассировку выполнения скрипта в ходе отладки. Немного изменив наш пример с открытием файла, получим:
import traceback
filename = 'my_data.dat'
try:
file = open(filename)
data = file.read()
except FileNotFoundError:
traceback.print_exc()
Если вы снова запустите код, то увидите на экране ту же информацию как и в примере сообщения об исключении выше, но при этом импортируемый модуль traceback предоставляет больше возможностей для трассировки вашей программы. Работа с пошаговыми трассировками выполнения вашего кода очень удобна для отладки.
Примеры, которые мы рассмотрели в этой статье, очень просты, но когда у вас проект с очень сложным кодом, с большим числом вложений, например, когда одна функция вызывает другую, которая в свою очередь создает объект, и запускает его метод и т. д. В этом случае узнать, что же вызвало возбуждение исключения и в каком месте вашего кода, становится сложной нетривиальной задачей.
Применение пользовательских исключений
Если вы будете заниматься разработкой пакетов, часто бывает полезно определить собственные типы исключений. Это дает большую гибкость вашим приложениям, поскольку позволяет другим разработчикам корректно обрабатывать их по своему усмотрению. И так давайте рассмотрим эту идею на следующем примере.
Представьте, что вы разрабатываете код, содержащий функцию, которая вычисляет среднее двух чисел, но вы хотите, чтобы оба эти числа были положительными. Это тот же пример иллюстрирует паттерн проектирования, известный как декоратор.
Начнем с определения нашей функции:
def average(x, y):
return (x + y)/2
Далее мы хотим возбуждать исключение Exception, в случае если любой из входных аргументов функции отрицателен. Для этого сделаем следующее:
def average(x, y):
if x<=0 or y<=0:
raise Exception('Both x and y should be positive')
return (x + y)/2
Если вы попробуете это вызвать нашу функцию с отрицательным параметром, в консоли вы увидите следующее:
Exception: Both x and y should be positive
Как видно, мы получили то, что хотели. Однако, если вы создаете пакет и ожидаете, что другие разработчики будут его использовать, было бы лучше определить пользовательское исключение, которое затем можно будет явно перехватить. С учётом сказанного наш код может выглядеть следующим образом:
class NonPositiveError(Exception):
pass
def average(x, y):
if x <= 0 or y <= 0:
raise NonPositiveError('Both x and y should be positive')
return (x + y) / 2
Объявляем класс нашего исключения, который будет наследовать от общего класса Exception. На данном этапе, мы просто будем выполнять инструкцию pass в теле нашего класса исключения.
Если мы запустим приведенный выше код с отрицательным значением входного параметра функции, то получим:
NonPositiveError: Both x and y should be positive
Теперь если вы захотите далее в коде перехватывать ваше пользовательское исключение типа NonPositiveError, то будете делать так как мы делали это ранее. Единственным отличием будет то, что пользовательские исключения по умолчанию недоступны, и их следует импортировать. Например, следующим образом:
from exceptions import NonPositiveError
from tools import average
try:
avg = average(1, -2)
except NonPositiveError:
avg = 0
Если вы достаточно долго работали с пакетами, то возможно уже сталкивались с большим разнообразием видов пользовательских исключений, подобных уже нами рассмотренным. Они являются отличным инструментом, позволяющим пользователю точно указать, что случилось при выполнении кода пакета, и далее действовать соответствующим образом. А значит мы должны быть готовы к обработке исключений различных типов: как пользовательских, так и общих, определенных в Python. Отметим, что в сообществе разработчиков Python приветствуется использование в своих пакетах и модулях пользовательских исключений, что облегчает их поддержку и использование.
Примеры хороших практик использования пользовательских исключений
Как мы уже говорили выше, при разработке пакета, очень удобно определять собственные пользовательские исключения, которые относятся только к выполнению кода пакета. Это значительно облегчает в дальнейшем разработку вашего приложения и дает другим разработчикам эффективный инструмент для определения проблем при выполнении кода в вашем пакете. Представьте, например, что вы работаете с приложением, использующим в качестве зависимости ваш пакет и хотите записывать какую-либо информацию в файл каждый раз, когда возбуждается исключение в коде вашего пакета.
Это достаточно легко, так из официальной документации известно, что типы всех исключений могут наследоваться от одного базового класса. Приведенный ниже пример кода достаточно большой, но включает в себя код всех примеров, рассмотренных нами выше, и поэтому его анализ будет несложен:
class MyException(BaseException):
pass
class NonPositiveIntegerError(MyException):
pass
class TooBigIntegerError(MyException):
pass
def average(x, y):
if x<=0 or y<=0: raise NonPositiveIntegerError('Either x or y is not positive') if x>10 or y>10:
raise TooBigIntegerError('Either x or y is too large')
return (x+y)/2
try:
average(1, -1)
except MyException as e:
print(e)
try:
average(11, 1)
except MyException as e:
print(e)
try:
average('a', 'b')
except MyException as e:
print(e)
print('Done')
Сначала определим свой класс исключений MyException, которой станет нашим базовым классом. Затем определим два класса исключений NonPositiveIntegerError и TooBigIntegerError, которые наследуют от MyException . Далее определим функцию average, но на этот раз в ней будет инициализироваться два разных типа исключений соответствующих случаям: если одно или оба из чисел, передаваемых в качестве аргумента, отрицательны или больше 10.
Далее в нашем примере представлены варианты вызова функции average с различными значениями аргументов. Отметим, что в блоке try/except мы всегда будем перехватывать исключение типа MyException, соответствующего нашему базовому классу, а не одно из конкретных типов исключений.
В первых двух примерах, передавая числа -1 и 11 в качестве аргументов, мы успешно выводим на экран сообщение об ошибке, и программа продолжает работать. Однако, если мы попытаемся вычислить среднее значение между двумя буквами (строками), возбужденное исключение будет иметь другую причину инициализации и не будет перехвачено блоком Exception. Поэтому программа аварийно завершит работу и вы увидите на экране следующее сообщение:
TypeError: '<=' not supported between instances of 'str' and 'int'
Таким образом, основным преимуществом использования классов для обработки пользовательских исключений заключается в возможности указания базового класса для перехвата всех исключений соответствующих классов-потомков.
Добавляем к исключениям аргументы
Иногда удобно использовать синтаксис аргументов в дополнении к инструкциям исключений, для предоставления пользователям более информативного форматированного вывода сообщений об ошибках. На примере нашей функции для вычисления среднего определим класс исключения NonPositiveIntegerError c использованием аргументов:
class MyException(BaseException):
pass
class NonPositiveIntegerError(MyException):
def __init__(self, x, y):
super(NonPositiveIntegerError, self).__init__()
if x<=0 and y<=0:
self.msg = 'Both x and y are negative: x={}, y={}'.format(x, y)
elif x<=0:
self.msg = 'Only x is negative: x={}'.format(x)
elif y<=0:
self.msg = 'Only y is negative: y={}'.format(y)
def __str__(self):
return self.msg
def average(x, y):
if x<=0 or y<=0:
raise NonPositiveIntegerError(x, y)
return (x+y)/2
try:
average(1, -1)
except MyException as e:
print(e)
Как видно из этого примера, инструкция возбуждения исключения принимает два аргумента, x и y, и генерирует на их основе форматированное сообщение. Отметим, что в нашем примере сообщение об ошибке может содержать информацию о том, что аргументы, переданные в функцию average, могут быть как оба отрицательны, так и о каждом непосредственно.
То есть использование аргументов при возбуждении и обработки исключений, предоставляет не только общую информацию об ошибке, но и более детальную о причине ее появления. И это очень удобно.
Ключевая часть в объявлении нашего пользовательского класса исключений находится в конце его объявления — это метод __str__. Этот метод отвечает непосредственно за то, что появляется на экране, когда вы используете инструкцию print(e) в блоке except . В нашем примере мы просто возвращаем сообщение, генерированное методом __init__, но многие разработчики предпочитают генерировать сообщение в методе __str__ на основе параметров, переданных в конструктор класса.
Выводы
Исключения в консоли — это то, что никто не хочет видеть в ходе разработки и отладки проекта, но на практике без них никак. Возможно, вы попытаетесь прочитать файл, который не существует, или пользователь, использующий вашу программу, ввёл в неё недопустимые значения, или же матрица данных, которые вы пытаетесь обработать, содержит число измерений, отличное от ожидаемых вами и т. д.
Обработка исключений является достаточной деликатной темой, поскольку она может привести к еще большему количеству проблем. Исключение — это сообщение вам о том, что с вашим кодом происходит что-то не так, и если вы не исправите ошибку должным образом, дальше станет еще хуже.
В тоже время обработка исключений может помочь вам избежать несоответствия входных данных ожидаемым, не корректно освобождать ресурсы, закрывать соединения со внешними устройствами, соединения по сети или корректно закрывать файлы с данными и т. д.
Использование инструкций try/except очень удобно если вы точно знаете, какие могут возбуждаться исключения и как их затем обработать. Поэтому при работе с ними необходимо предусмотреть все возможные ситуации и спланировать перехват возбужденных исключений таким образом, чтобы работа программы не завершилась аварийно и не произошло утечек ресурсов и потери данных.
Как и в любой другой теме, посвященной Python или другому языку программирования, лучший способ изучить его — внимательно посмотреть на код другого и проанализировать его. Не все пакеты определяют свои собственные исключения и не обрабатывают их одинаково. Если вы ищете вдохновение, вы можете посмотреть ошибки Pint , в его небольшом простом пакете, или исключения Django, как пример гораздо более сложного проекта.
Код, и текст статьи доступны по ссылкам и если у вас есть какие-либо комментарии или предложения по его улучшению, пишите.