Любая программа может выполняться с ошибками. Часть ошибок связана с самим кодом. А другая часть связана с ситуациями, которые возникают довольно редко, но означают, что дальнейшее выполнение программы невозможно, если возникшую проблему никак не решить. Такие ситуации исключительны. Поэтому и механизм языка, предназначенный для работы с исключительными ситуациями, называется системой исключений (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("Query execution error: '{}'".format(e.query))
except DBConnectionError as e:
print("Can't connect to DB: '{}'".format(e.status))
...
Но что делать, если вы перехватили исключение, сделали некие необходимые действия, а затем решили "пробросить" исключение выше. Большинству новичков приходит на ум строчка raise e
. Но это плохая идея: так вы получите возбуждение нового исключения, пусть даже и представляющего старый объект, а значит в traceback местом возникновения исключения будет уже эта самая строчка raise e
! Обычно вы не хотите терять информацию о месте возникновения изначальной исключительной ситуации, поэтому просто пишите raise
— так будет заново возбуждено последнее перехваченное исключение!
Перехватывать исключения можно неправильно. Про неправильный порядок обработчиков я выше уже писал. Есть и другая типичная ошибка, которую, к счастью, обычно находит линтер: except Exception:
(или просто except:
). Чем же плох такой способ поймать сразу все исключения? Тем, что можно случайно поймать то, что ловить совсем даже не нужно, например ошибку "переменная не объявлена":
try:
bla-bla
except:
pass # теперь мы не увидим, что переменная "bla" не была объявлена!
Совет: всегда перехватывайте только те исключения, которые ожидаете и собираетесь обработать именно в месте перехвата, а все остальные исключения пусть "всплывают" выше!
Единственный случай, в котором допустимо перехватывать Exception
, это случай, когда в конце обработчика исключение возбуждается заново. Таким способом вы можете, например, протоколировать (логировать, записывать в log-файл) все ошибки, но никак их не обрабатывать, чтобы у вызывающей стороны была возможность отреагировать на ошибку.
Ещё один антипаттерн: огромные try
-блоки. Очень редко встречается действительно большой участок кода, в котором могут произойти всего несколько ожидаемых исключений. Практически всегда лучше перехватывать конкретные исключения в небольших участках кода, а в других участках не перехватывать ничего. Так вы не окажетесь в режиме "защитного программирования", когда все исключения всегда ловятся максимально рано, из-за чего код становится очень сложно читать.
Совет: перехватывайте только те ошибки, обработка которых позволит продолжить выполнение текущего участка кода!
Вам ответят команда поддержки Хекслета или другие студенты.
Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.
Загляните в раздел «Обсуждение»:
Профессиональная подписка откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.
Наши выпускники работают в компаниях:
Зарегистрируйтесь или войдите в свой аккаунт