Зарегистрируйтесь, чтобы продолжить обучение

Исключения Python: Введение в ООП

Любая программа может выполняться с ошибками. Часть ошибок связана с самим кодом. А другая часть связана с ситуациями, которые возникают довольно редко, но означают, что дальнейшее выполнение программы невозможно, если возникшую проблему никак не решить. Такие ситуации исключительны. Поэтому и механизм языка, предназначенный для работы с исключительными ситуациями, называется системой исключений (exceptions).

Пример исключительной ситуации: ошибка IndexError. Эту ошибку вы видите, когда обращаетесь к строке, списку, кортежу по индексу, который выходит за границы коллекции. Как правило, с коллекциями вы работаете с помощью цикла for, или же не забываете следить за тем, чтобы индекс не достигал длины списка. А значит ситуация выхода индекса за допустимые границы — исключительная!

Когда вы открываете файл на запись, а место на диске внезапно заканчивается или у вас не оказывается прав на запись в этот файл — все это тоже исключительные ситуации.

При этом вы не можете продолжать выполнять программу, если в файл не получается записать или элемента в списке по вычисленному индексу не окажется. Выполнение должно прерваться максимально быстро, чтобы программа не успела ухудшить ситуацию.

Генерация исключения в коде похожа на 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-блоки. Очень редко встречается действительно большой участок кода, в котором могут произойти всего несколько ожидаемых исключений. Практически всегда лучше перехватывать конкретные исключения в небольших участках кода, а в других участках не перехватывать ничего. Так вы не окажетесь в режиме "защитного программирования", когда все исключения всегда ловятся максимально рано, из-за чего код становится очень сложно читать.

Совет: перехватывайте только те ошибки, обработка которых позволит продолжить выполнение текущего участка кода!


Дополнительные материалы

  1. Исключения
  2. Иерархия исключений

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

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

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка веб-приложений на Django
10 месяцев
с нуля
Старт 26 декабря

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»