- Иерархия исключений
- Иерархии классов, isinstance и issubclass
- Синтаксис, наконец-то!
- Ветка finally
- Получение экземпляра исключения и возбуждение уже пойманного
- Антипаттерны при перехвате исключений
Любая программа может выполняться с ошибками. Часть ошибок связана с самим кодом. А другая часть связана с ситуациями, которые возникают довольно редко, но означают, что дальнейшее выполнение программы невозможно, если возникшую проблему никак не решить. Такие ситуации исключительны. Поэтому и механизм языка, предназначенный для работы с исключительными ситуациями, называется системой исключений (exceptions).
Пример исключительной ситуации: ошибка IndexError
. Эту ошибку вы видите, когда обращаетесь к строке, списку, кортежу по индексу, который выходит за границы коллекции. Как правило, с коллекциями вы работаете с помощью цикла for
, или же не забываете следить за тем, чтобы индекс не достигал длины списка. А значит ситуация выхода индекса за допустимые границы — исключительная!
Когда вы открываете файл на запись, а место на диске внезапно заканчивается или у вас не оказывается прав на запись в этот файл — все это тоже исключительные ситуации.
При этом вы не можете продолжать выполнять программу, если в файл не получается записать или элемента в списке по вычисленному индексу не окажется. Выполнение должно прерваться максимально быстро, чтобы программа не успела ухудшить ситуацию.
Возбуждение (raising) исключения в коде похоже на return
, только на его глобальную версию, завершающую все функции в порядке, обратном тому, в котором они вызывались. Если исключение будет возбуждено, но не будет перехвачено, то есть как-то обработано, вся программа так и завершится и вы увидите распечатку трейсбэка (traceback). Там-то и будет отображена та самая ошибка IndexError
(или какая-то другая).
Иерархия исключений
Исключения в современном языке программирования с богатой системной библиотекой могут быть самыми разными и представлены во множестве. Однако почти всегда исключения объединяются в иерархию исключений. Так все "ошибка ввода-вывода"/IOError
или ошибка "файл не найден"/FileNotFoundError
наследуются от исключения OSError
("ошибки взаимодействия с Операционной Системой"), которое наследуется от Exception
("просто некое исключение"). IndexError
и KeyError
("ключ (словаря) не найден") являются потомками LookupError
("ошибка поиска чего-то"), которое является потомком Exception
.
Иерархия исключений, таким образом, представляет собой дерево, корнем которого является BaseException
, стволом — Exception
, а дальше происходит ветвление на виды исключений, а затем — на конкретные исключения.
Зачем же нужно было придумывать эту самую иерархию исключений? Чтобы можно было "ловить" исключения как по одному (ловить IndexError
), так и перехватывать целые группы (OSError
).
Иерархии классов, isinstance
и issubclass
Функция isinstance
уже упоминалась ранее в этом курсе: она служит для определения, является ли объект экземпляром некоего класса. Но функция на самом деле делает чуть больше: функция проверяет, не является ли класс предком класса, экземпляр которого мы исследуем. Пример:
isinstance(7, object) # True
7
является экземпляром класса int
, но int
является потомком класса object
, поэтому в какой-то мере семерка, и правду, является экземпляром класса object
!
У isinstance
есть функция-близнец issubclass
. Эта проверяет родство классов:
issubclass(int, object) # True
issubclass(IndexError, LookupError) # True
issubclass(IndexError, Exception) # True
Последние два примера интересны в контексте данной темы: именно так и можно проверить, что некое исключение входит в нужную группу. Иерархия исключений во многом ради возможности делать такие проверки и создавалась!
Синтаксис, наконец-то!
Вот так выглядит возбуждение исключения:
raise ValueError('Age too low!')
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# ValueError: Age too low!
Ключевое слово raise
принимает в качестве аргумента экземпляр какого-либо класса, являющегося потомком BaseException
. Большинство исключений принимают в качестве параметра строковое сообщение, описывающее конкретную ситуацию.
А так выглядит перехват исключений:
l = []
try:
l[100500] = 42
except IndexError:
print('Catched!')
# => Catched!
try:
начинает блок, при выполнении которого могут возникать исключения. Следом идут одна или несколько веток except
, которые описывают базовый класс исключений, которые будут перехватываться. Если возникшее исключение подошло — класс исключения оказался потомком от указанного базового класса или самим указанным классом, то будет выполнен код обработчика. В данном примере вместо ошибки мы видим печать сообщения.
Важно помнить, что если у вас указано несколько веток except
, то первыми нужно указывать наиболее конкретные ветки. Иначе вы можете оказаться в ситуации вроде этой:
try:
user = users[input('May I have your name? ')]
except Exception:
sys.exit(1) # молча завершаем программу
except (KeyError, IndexError):
print('No users with such name found!')
Здесь ветка except Exception:
отлавливает вообще все исключения, ведь любое конкретное исключение косвенно будет экземпляром Exception
! Увы, вторая ветка except
не имеет шанса хоть раз быть выполненной :(
Отметьте, что во второй ветке мы указали кортеж, содержащий несколько классов исключений. Таким образом можно указать обработчик, который будет перехватывать несколько видов исключений.
Ветка finally
Иногда не нужно отлавливать исключения в конкретном блоке кода. Например, вы хотите поймать исключение где-то выше или вообще ничего не ловить. Однако просто так прерывать выполнение кода нельзя, потому что требуется некое действие вроде закрытия открытого файла. В таких случаях применяют ветку finally
:
f = open('data.txt')
try:
text = f.read()
words = len(text.split())
finally:
f.close()
В этом коде файл будет закрыт (f.close()
) в любом случае, вне зависимости от того, произошла ошибка или нет. Если ошибка все же произошла, то сразу после блока finally
выполнение кода будет прервано и исключение "всплывет" выше, но хотя бы по поводу незакрытого файла можно будет не волноваться!
Ветка finally
может использоваться вместе с ветками except
, но должна идти самой последней.
Получение экземпляра исключения и возбуждение уже пойманного
Часто вы хотите получить доступ к сообщению исключения или каким-то дополнительным данным: более специфичные для предметной области исключения могут иметь специальные атрибуты, полезные при отладке. Для этого нужно указать в ветке except
имя переменной, которая получит ссылку на экземпляр исключения:
try:
...
except (SQLSelectError, SQLInsertError) as e:
print(f"Query execution error: '{e.query}'")
except DBConnectionError as e:
print(f"Can't connect to DB: '{e.status}'")
...
Но что делать, если вы перехватили исключение, сделали некие необходимые действия, а затем решили "пробросить" исключение выше. Большинству новичков приходит на ум строчка raise e
. Но это плохая идея: так вы получите возбуждение нового исключения, пусть даже и представляющего старый объект, а значит в traceback местом возникновения исключения будет уже эта самая строчка raise e
! Обычно вы не хотите терять информацию о месте возникновения изначальной исключительной ситуации, поэтому просто пишите raise
— так будет заново возбуждено последнее перехваченное исключение!
Антипаттерны при перехвате исключений
Перехватывать исключения можно неправильно. Неправильный порядок обработчиков мы уже обсуждали выше. Есть и другая типичная ошибка, которую, к счастью, обычно находит линтер: except Exception:
(или просто except:
). Чем же плох такой способ поймать сразу все исключения? Тем, что можно случайно поймать то, что ловить совсем даже не нужно, например ошибку "переменная не объявлена":
try:
bla-bla
except:
pass # теперь мы не увидим, что переменная "bla" не была объявлена!
Совет: всегда перехватывайте только те исключения, которые ожидаете и собираетесь обработать именно в месте перехвата, а все остальные исключения пусть "всплывают" выше!
Единственный случай, в котором допустимо перехватывать Exception
, это случай, когда в конце обработчика исключение возбуждается заново. Таким способом вы можете, например, протоколировать (логировать, записывать в log-файл) все ошибки, но никак их не обрабатывать, чтобы у вызывающей стороны была возможность отреагировать на ошибку.
Еще один антипаттерн: огромные try
-блоки. Очень редко встречается действительно большой участок кода, в котором могут произойти всего несколько ожидаемых исключений. Практически всегда лучше перехватывать конкретные исключения в небольших участках кода, а в других участках не перехватывать ничего. Так вы не окажетесь в режиме "защитного программирования", когда все исключения всегда ловятся максимально рано, из-за чего код становится очень сложно читать.
Совет: перехватывайте только те ошибки, обработка которых позволит продолжить выполнение текущего участка кода!
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.