В предыдущих видео мы несколько раз упоминали про исключения в Python. Сегодня мы как раз обсудим то, как устроены исключения в Python. Мы поговорим про генерацию исключений, что при этом происходит, обсудим типы исключений, а также рассмотрим, как обрабатывать исключения в Python. Для начала давайте попробуем вызвать исключения и посмотрим, что при этом произойдет. Для этого нам потребуется консоль. Давайте попробуем написать простенькую программу на Python. Пусть это будет файл zero.py. Попробуем вывести строчку 1/0. Давайте запустим нашу программу. Итак, мы видим, что произошло. При делении на 0 возникло исключение, и при возникновении исключения на стандартный вывод печатается информация о его типе, дополнительная информация о исключении, а также стек вызовов. Давайте посмотрим, что у нас произошло. Мы видим, на экран напечаталось - тип исключения — ZeroDivisionError, дополнительная информация и сам стек. Стек у нас пока небольшой, и для того чтобы посмотреть на стек настоящей программы, давайте посмотрим дополнительный пример. Пусть у нас есть программа, которая считает количество строк в исходном файле, который подали на вход. Выглядит она нам не очень интересно как, то есть есть некая функция main, в функции main вызывается другая функция. Эта функция вызывает функцию wc с переданным файлом и печатает на экран некую строчку с именем файла и количеством строк в нем. А функция wc открывает этот файл и итератором проходится по всем строкам в этом файле. Давайте попробуем вызвать данную программу, передадим ей либо несуществующий файл, либо файл, недоступный для чтения, и посмотрим, что при этом произойдет. Итак, у нас есть наша программа. Давайте попробуем ей передать файл, в котором хранятся пароли в Linux. Это файл etc/shadow. Итак, как видим, сгенерировалось исключение. При этом Python остановил свою работу, и на экране мы видим тип исключения — это PermissionError, а также дополнительную информацию - код ошибки, текстовые сообщения, что доступ к файлу etc/shadow запрещен. Также теперь мы видим стек вызовов, теперь он у нас побольше. Давайте распечатаем текст самой программы и немножко разберемся, что же значит этот стек вызовов. Давайте посмотрим, что на строке 5 фукнции wc была вызвана функция open. Давайте еще раз сгенерируем наше исключение. Итак, мы сгенерировали исключение, и посмотрим на текст нашей программы. Итак, мы видим, что в строке 5 нашей программы была вызвана функция open. Именно это привело к генерации исключения. Давайте дальше пройдемся по стеку вызовов и раскрутим его наверх. Мы видим, что наша функция wc была вызвана на строке 12, вот видим ее. Это было сделано функцией process_file. Если мы будем раскручивать стек дальше, то мы сможем отследить всю последовательность вызовов функции, которая привела к исключению. Таким образом, это нам очень может сильно помочь, если мы будем разбираться с чужими исключениями, которые вдруг внезапно возникли в программе и не были обработаны программистом. Какие типы исключений бывают? В Python бывают по большому счету два типа исключений. Первый — это исключения из стандартной библиотеки в Python. Они генерируются, собственно, самой библиотекой. То есть когда мы вызываем функцию стандартной библиотеки, например, мы видели, как функция open сгенерировала исключение PermissionError. А также второй тип исключений — это пользовательские исключения. Они могут быть сгенерированы и обработаны самим программистом при написании программ на Python. Давайте посмотрим на иерархию исключений в стандартной библиотеке Python. Все исключения наследуются от базового класса BaseException. Существуют несколько системных исключений, например, SystemExit — это исключение генерируется, если мы вызвали функцию OSExit. KeyboardInterrupt — это исключение генерируется, если мы нажали сочетание клавиш Ctrl + C и так далее. Все остальные исключения генерируется от базового класса Exception. Именно от этого класса нужно будет порождать и свои исключения, чем мы и займемся дальше. Давайте посмотрим и обсудим некоторые исключения из базы библиотеки, такие как, например, AttributeError, IndexError, KeyError, TypeError, и попробуем их вызвать. Для этого нам снова потребуется консоль. Давайте запустим интерпретатор. Объявим простенький класс. Пусть это будет класс, который ничего не делает. Давайте создадим объект этого класса и попробуем обратиться к атрибуту foo. Как мы видим, сгенерировалось исключение AttributeError. Давайте попробуем объявить словарик. Пусть в нем будет один элемент. Попробуем обратиться к несуществующему ключику. Итак, у нас получилось сгенерировать исключение KeyError. Если бы мы объявили список из двух элементов и обратились, например, к 10-му элементу, у нас бы сгенерировался IndexError. Также если мы, например, попробовали преобразовать к целому числу строчку, мы бы получили ValueError. Или если бы попробовали сложить число и строку, получили бы TypeError. Все эти исключения — из стандартной библиотеки. Вам при работе очень часто придется сталкиваться с ними, и теперь вы знаете, как они выглядят, и при каких обстоятельствах они могут быть сгенерированы. Если исключение сгенерировано, то, как я уже говорил, Python-интерпретатор остановит свою работу и на экран будет выведен стек вызовов и информация о типе исключений. И нам это не всегда хочется, чтобы такое поведение было по умолчанию, и поэтому нужно как-то обработать сгенерированные исключения. Обработать его можно при помощи блока try except. То есть у нас есть код, который потенциально может генерировать исключения, мы этот код обрамляем в блок try except, и тем самым при генерации исключений управление будет передано в блок except. Таким образом можно отловить все исключения, которые генерируются в блоке try except. Если мы в блоке except укажем исключение, например в данном случае Exception, то мы будем отлавливать исключения всех типов, у которых класс Exception является родителем. В целом неправильно ждать любые исключения, и это может привести к непредвиденным сюрпризам работы вашей программы. Давайте рассмотрим следующий пример. Мы в бесконечном цикле просим пользователя ввести число, преобразовываем его строку к числу целому. В данном случае может возникнуть исключение ValueError. Однако, мы в своей программе отлавливаем все исключения. Давайте попробуем ее исполнить и посмотрим, как она работает. Так, нас просят ввести число. Давайте введем. Нас снова просят ввести число. Давайте, например, попросим наш интерпретатор завершить свою работу и нажмем Ctrl + C. Как мы видим, у нас это не получилось сделать, то есть наша программа стала вести себя непредвиденным образом. Она нас снова опять заставляет ввести число. Это как раз говорит о том, что нужно обрабатывать конкретные исключения, и в данном случае правильным вариантом данной программы была бы обработка исключений ValueError. Если мы попробуем снова запустить данную программу и нажмем Ctrl--. Давайте введем сначала неправильное число, нажмем Ctrl + C, то возникнет исключение и его никто не обработает, и наша программа завершит свою работу. Поэтому не следует обрабатывать все исключения и оставлять пустым блок except. Имейте это в виду. Также у блока try except может быть блок else. Блок else вызывается в том случае, если никакого исключения не произошло. Если нам нужно обработать несколько исключений, мы можем использовать несколько блоков except и указать разные классы для обработки исключения. Причем в каждом блоке except может быть свой собственный обработчик. Например, если мы в данном примере ввели некорректное число, то мы еще раз продолжим работу нашего цикла. В противном случае, если мы нажали Ctrl + C, то мы прекратим цикл. Именно таким образом можно обработать несколько исключений. Если обработчик исключений выглядит одинаково, то несколько исключений можно передать в виде списка в блок except и также обработать сгенерированные исключения. В данном случае мы ожидаем два исключения, например, что пользователь ввел некорректное число, либо если он ввел 0, то данная программа сгенерирует ZeroDivisionError, и мы его отловим. Мы уже с вами обсуждали классы и наследования в классах, и вот у exception'ов есть своя иерархия, и сделана она неспроста. Также при помощи родительского класса можно обрабатывать несколько исключений. Давайте, например, посмотрим на иерархию классов LookupError. Этот класс является родительским для исключений IndexError и KeyError. Мы можем это проверить при помощи известных нам функций issubclass. Рассмотрим следующий пример. У нас есть некая структура данных, это наша база данных, которая хранит по цветам надписи на футболках. Нам необходимо получать доступ к этим надписям по цветам. Пользователя просим ввести цвет, просим ввести номер по порядку, а затем обращаемся к нашей структуре данных по ключу, а затем по индексу. И если пользователь введет, например, некорректный цвет, то будет сгенерировано исключение KeyError, а если пользователь введет недопустимый индекс, то будет вызвано исключение IndexError. Все эти исключения мы можем обработать при помощи базового класса LookupError. Иногда это очень удобно и требуется для как раз обработки пользовательских исключений. Также у исключений есть дополнительный блок finally. Рассмотрим проблему. Например, мы открываем файл, читаем строки, обрабатываем как-то эти строки, и в процессе работы нашей программы возникает исключение, которое мы не ждем, например. В таком случае файл закрыт не будет. У нас эти открытые файловые дескрипторы могут накапливаться, что, в принципе, не хорошо. Таким же образом могут накапливаться открытые сокеты, или не освобождаться память, всё что угодно. Для контроля таких ситуаций существуют, во-первых, контекстные менеджеры, а во-вторых, можно использовать блок finally в исключениях. Выглядит это таким образом, как представлено на слайде. Мы пишем блок finally и вызываем метод close для нашего объекта f. В данном случае блок finally будет выполнен в любом случае. Возникло исключение или не возникло — блок finally будет выполнен. Итак, мы поговорили с вами про исключения, посмотрели на то, как они выглядят, как выглядит stack trace, обсудили, как ведет себя Python при генерации исключений. Также посмотрели на то, какие типы исключений бывают - это пользовательские исключения и исключения стандартной библиотеки. Также рассмотрели иерархию исключений в стандартной библиотеке и поговорили про их обработку. Мы продолжим обсуждать то, как устроены исключения, в следующем видео.