Ошибки. Обработка исключений

Механизм обработки исключений

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

Но сначала разберёмся, какие ошибки в принципе могут возникать при программировании. Основных типов ошибок три:

  • Синтаксические (интерпретатор или компилятор отказывается обрабатывать программу). Как правило, исправляются очень легко и отсутствуют в окончательном отлаженном варианте программы.
  • Логические (программа выполняет неверные действия). Могут исправляться как очень легко, так и необычайно трудно, вплоть до переписывания какой-то части программы.
  • Исключительные ситуации, которые зависят от входных данных или внешних условий, которые программист не может предотвратить (деление на ноль, отсутствие файла, недостаточные права для работы с файлом и другие). Такие ошибки должны определяться или предотвращаться самим программистом путём добавления соответствующих инструкций в программу.

На самом деле, эта классификация не совсем чёткая. Так, например, в языке Си использование несуществующей переменной приводит к ошибке компиляции, а в Python3 – к ошибке времени выполнения, которая не проявится, пока не будет выполнена соответствующая строка. Аналогичная ситуация и с ошибками типов, которые проявляются в Python3 и других динамически типизированных языках в момент выполнения строки с ошибкой.

В Python3 существует специальный механизм, позволяющий перехватывать ошибки. Ошибки в данном механизме являются частным случаем различных исключительных ситуаций (исключений). Пример:

try:
    # В блоке try находятся потеницально опасные инструкции,
    # которые могут привести к ошибке
    x = 1 / 0
    print("Эта строка не выведется, так как в предыдущей ошибка)
except ZeroDivisionError:
    # В блоке except находится обработчик ошибки, в данном случае,
    # ошибки деления на ноль
    print("Перехвачено деление на ноль!")
print("Продолжаем нормальное выполнение программы")

Если ошибка перехвачена, то программа завершена не будет, а сначала выполнит блок обработки ошибок except, затем перейдёт к первой инструкции вне его.

Как следует из названия исключений, они предназначены для работы с третьим видом ошибок, но в Python3 механизм исключений позволяет обрабатывать и часть логических ошибок (неправильный индекс массива). Даже ошибки первого типа, такие как неопределённая переменная, приводят в Python3 к ошибке времени выполнения, и так же могут быть перехвачены. Тем не менее, надо стремиться к тому, чтобы максимально искоренить ошибки первых двух типов, повысив тем самым качество программы, а ошибки третьего типа уже обрабатывать с помощью механизма исключений.

def f(a):
    if a == 1:
        x = 1 / 0
    else:
        L = []
        x = L[0]

def g(a):
    try:
        f(a)
        print(1)
    except ZeroDivisionError:
        print(2)
    print(3)


def h():
    try:
        g(a)
        print(4)
    except ZeroDivisionError:
        print(5)
    except IndexError:
        print(6)
    print(7)

Вызов функции h может давать два варианта вывода чисел: для h(1), когда в функции f произойдёт деление на 0, и для h(2) (или вообще от любого другого аргумента, не равного 1). Во втором случае это связано с ошибкой индекса при попытке извлечь элемент из пустого списка.

вызов h(1) h(2)
выведет 2 3 4 7 6 7

В первом случае, функция h(1) вызовет g(1), g(1) вызовет f(1). В функции происходит ZeroDivisionError (если вы не помните имя исключения, вы всегда можете его увидеть на последней строке сообщения об ошибке, которое выведет интерпретатор Python3). Инструкция print(1) не выполнится, так как в блоке try в функции g произошла ошибка, нормальное выполнение программы прервано, и запущен поиск обработчика. Обработчик будет найден в самой функции g, это print(2), после выполнения обработчика выполнение программы продолжится обычным образом. Будет выведено print(3), затем выполнение выйдет в функцию h, выведет print(4), и спокойно перейдёт к print(7). Обратите ещё раз внимание, что print(5) не выполнится, так как ZeroDivisionError уже обработана в функции g.

Во втором случае, когда в f произойдёт ошибка, обработчик не будет найден ни в функции f, ни в функции g, и только в функции h ошибка IndexError будет обработана инструкцией print(6), затем выполнение продолжится нормальным образом и перейдёт к print(7).

Процесс поиска обработчика подробно показан на рисунке.

_images/stack.png

Общий вид блока перехвата ошибок

try:
        код, который может привести к ошибке
except имяИсключения1, ...,  имяИсключенияN:
        код обработчика, который будет выполнен,  если случится любое из указанных
        исключений
except имяДругогоИсключения1, ...,  имяДругогоИсключенияM:
        другой обработчик для других исключительных ситуаций
else:
        код, который выполнится, если ошибки не было
finally:
        выполняется всегда, если исключение было, если его не было и даже если ошибка
        произошла в обработчике. Применяется для того, чтобы при любом исходе
        закрыть файл или освободить ресурс.

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

В питоне каждый тип исключения – это тип данных (вообще, как мы узнаем из следующей главы, исключения – это классы). И у исключений могут быть дочерние типы исключений. Например, у исключения ArithmeticError есть дочерние исключения FloatingPointError (ошибка числа с плавающей точкой), OverflowError (Ошибка переполенния) и ZeroDivisionError (деление на ноль). Если установить обработчик на ArithmeticError, он сработает так же, если случится любое из этих трёх дочерних исключений.

Иерархия исключений в Python3 выглядит следующим образом:

BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- StopAsyncIteration
      +-- ArithmeticError
      |    +-- FloatingPointError
      |    +-- OverflowError
      |    +-- ZeroDivisionError
      +-- AssertionError
      +-- AttributeError
      +-- BufferError
      +-- EOFError
      +-- ImportError
      +-- 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
           +-- ResourceWarning

Тестирование программ

def test():
    b = create_book()
    assert b == []
    add_user(b, 'John', '123')
    assert b == [['John', '123']]
    add_user(b, 'Vasya', '456')
    assert b == [['John', '123'], ['Vasya', '456']]
    tel = find_tel(b, 'Vasya')
    assert tel = '456'
    remove_by_name(b, 'John')
    assert b == [['Vasya', '456']]

Assert (от английского assert – утверждать) – оператор, проверяющий выражение на истинность. Если выражение истинно, не произойдёт ничего, и начнёт выполняться следующая инструкция, но если оно ложно, программа завершится с ошибкой AssertionError.

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

Ошибки AssertionError не рекомендуется перехватывать, так как они говорят о том, что программа уже начала работать не так, как ожидал программист, и при попытке продолжить работу дальше, она, скорее всего, сломается окончательно. Если ваша программа не поддаётся автоматическому тестированию, она плохо написана.