8. Ошибки и исключения

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

8.1. Синтаксические ошибки

Синтаксические ошибки, также известные как ошибки разбора кода — вероятно, наиболее привычный вид жалоб компилятора, попадающихся вам при изучении Python:

>>> while True print('Привет мир')
  File "<stdin>", line 1
    while True print('Привет мир')
                   ^
SyntaxError: invalid syntax

Парсер повторно выводит ошибочную строку и отображает небольшую «стрелку», указывающую на самую первую позицию в строке, где была обнаружена ошибка. Причина ошибки (или по крайней мере место обнаружения) находится в символе, предшествующем указанному: в приведённом примере ошибка обнаружена на месте вызова функции print() поскольку перед ним пропущено двоеточие (':'). Также здесь выводятся имя файла и номер строки, благодаря этому вы знаете в каком месте искать, если ввод был сделан из сценария.

8.2. Исключения

Даже если выражение или оператор синтаксически верны, они могут вызвать ошибку при попытке их исполнения. Ошибки, обнаруженные при исполнении, называются исключениями. Они не фатальны: позже вы научитесь перехватывать их в программах на Python. Большинство исключений, правда, как правило, не обрабатываются программами и приводят к сообщениям об ошибке, таким как следующие:

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't convert 'int' object to str implicitly

Последняя строка сообщения об ошибке поясняет произошедшее. Исключения представлены различными типами и тип исключения выводится в качестве части сообщения: в примере это типы ZeroDivisionError, NameError и TypeError. Часть строки, определяющая тип исключения — это имя произошедшего встроенного исключения. Такое утверждение верно для всех встроенных исключений, но не обязано быть истинным для исключений, определённых пользователем (однако, само соглашение — довольно полезное). Имена стандартных исключений — это встроенные идентификаторы (не ключевые слова).

Оставшаяся часть строки определяет детали произошедшего на основе типа исключения, которое было его причиной.

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

В разделе Встроенные исключения перечисляются встроенные исключения и их значения.

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

Существует возможность написать код, который будет перехватывать избранные исключения. Посмотрите на представленный пример, в котором пользователю предлагают вводить число до тех пор, пока оно не окажется корректным целым. Тем не менее пользователь может прервать программу (используя сочетание клавиш Control-C или какое-либо другое, поддерживаемое операционной системой); заметьте — о вызванном пользователем прерывании сигнализирует исключение KeyboardInterrupt.

>>> while True:
...     try:
...         x = int(input("Пожалуйста, введите число: "))
...         break
...     except ValueError:
...         print("Ой! Это было неверное число. Попробуй ещё раз...")
...

Оператор try работает следующим образом.

  • Сначала выполняется try блок (операторы между ключевыми словами try и except).
  • При отсутствии исключений except блок пропускается и выполнение try оператора завершается.
  • Если во время выполнения предложения try вызывается исключение, остальная часть блока пропускается. Затем, если тип этого исключения совпадает с исключением, указанным после ключевого слова except, выполняется блок except, а по его завершению выполнение продолжается сразу после оператора try.
  • Если порождается исключение, не совпадающее по типу с указанным в блоке except — оно передаётся внешнему оператору try; если ни одного обработчика не найдено, исключение считается необработанным, выполнение полностью останавливается и выводится сообщение, схожее с показанным выше.

У оператора try может быть более одного блока except — для описания обработчиков различных исключений. При этом будет выполнен максимум один обработчик. Обработчики ловят только те исключения, которые вызываются внутри соответствующего блока try, но не те, вызываемые в других обработчиках этого же самого оператора try. Блок except может указывать несколько исключений в виде заключённого в скобки кортежа, например:

... except (RuntimeError, TypeError, NameError):
...     pass

Класс в блоке except совместим с исключением, если он является тем же классом или его базовым классом (но не наоборот — блок, перечисляющий производный класс, несовместим с базовым классом). Например, следующий код будет печатать B, C, D в таком порядке:

class B(Exception):
    pass

class C(B):
    pass

class D(C):
    pass

for cls in [B, C, D]:
    try:
        raise cls()
    except D:
        print("D")
    except C:
        print("C")
    except B:
        print("B")

Обратите внимание: если блоки except были поменяны местами (сначала с except B), напечатал бы B, B, B — срабатывает первое соответствие, за исключением предложения.

В последнем блоке except можно не указывать имени (или имён) исключений. Используйте это с особой осторожностью, так как таким образом легко скрыть настоящую программную ошибку! Также такой обработчик может быть использован для вывода сообщения об ошибке и порождения исключения заново (позволяя при этом обработать исключение коду, вызвавшему обработчик):

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("Ошибка ОС: {0}".format(err))
except ValueError:
    print("Не удалось преобразовать данные в целое число.")
except:
    print("Непредвиденная ошибка:", sys.exc_info()[0])
    raise

У оператора tryexcept есть необязательный блок else, который, если присутствует, должен размещаться после всех блоков except. Его полезно использовать при наличии кода, который должен быть выполнен, если блок try не породил исключений. Например:

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except OSError:
        print('не могу открыть', arg)
    else:
        print(arg, 'имеет', len(f.readlines()), 'строк')
        f.close()

Использование блока else предпочтительнее, чем добавление дополнительного кода к блоку try, поскольку исключает неожиданный перехват исключения, которое появилось не по причине выполнения кода, защищенного оператором tryexcept.

При появлении исключения, оно может иметь ассоциированное значение, также известное как аргумент исключения. Присутствие и тип аргумента зависят от типа самого исключения.

В блоке except можно указать переменную, следующую за именем исключения. Переменная связывается с экземпляром исключения, аргументы которого хранятся в instance.args. Для удобства, экземпляр исключения определяет метод __str__(), так что вывод аргументов может быть произведён явно, без необходимости отсылки к .args. Таким образом, вы также можете создать/взять экземпляр исключения перед его порождением и добавить к нему атрибуты по желанию.

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))    # экземпляр исключения
...     print(inst.args)     # аргументы хранятся в .args
...     print(inst)          # __str__ позволяет напрямую печатать аргументы,
...                          # но может быть переопределено в подклассах исключений
...     x, y = inst.args     # распаковать аргументы
...     print('x =', x)
...     print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs

Если у исключения есть аргументы, они выводятся в качестве последней («детальной») части сообщения о необработанном исключении.

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

>>> def this_fails():
...     x = 1/0
...
>>> try:
...     this_fails()
... except ZeroDivisionError as err:
...     print('Обработка ошибок времени выполнения:', err)
...
Обработка ошибок времени выполнения: division by zero

8.4. Вызов исключений

Оператор raise позволяет программисту принудительно породить исключение. Например:

>>> raise NameError('Всем привет')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: Всем привет

Единственный аргумент оператора raise определяет исключение, которое нужно вызвать. Им может быть либо экземпляр исключения, либо класс исключения (класс, дочерний к классу Exception). Если класс исключения будет передан, он неявно создаётся путём вызова конструктора без аргументов:

raise ValueError  # сокращение для 'raise ValueError()'

Если вам нужно определить, было ли возбуждено исключение, не перехватывая его — упрощённая форма оператора raise позволит возбудить исключение заново:

>>> try:
...     raise NameError('Всем привет')
... except NameError:
...     print('Пролетело исключение!')
...     raise
...
An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
NameError: Всем привет

8.5. Исключения, определенные пользователями

В программах можно определять свои исключения, посредством создания нового класса исключения (см. Классы подробнее о Python классах). В общем случае, исключения должны наследоваться от класса Exception явно или неявно.

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

class Error(Exception):
    """Базовый класс для исключений в этом модуле."""
    pass

class InputError(Error):
    """Вызывается исключение из-за ошибок на вводе.

    Атрибуты:
        expression -- входное выражение, в котором произошла ошибка
        message -- объяснение ошибки
    """

    def __init__(self, expression, message):
        self.expression = expression
        self.message = message

class TransitionError(Error):
    """Вызывается, когда операция пытается выполнить недопустимый переход
    состояния.

    Атрибуты:
        previous -- состояние в начале перехода
        next -- попытка нового состояния
        message -- объяснение того, почему переход не разрешён
    """

    def __init__(self, previous, next, message):
        self.previous = previous
        self.next = next
        self.message = message

Большинство исключений определяется с именем оканчивающееся на «Error», подобно именованию стандартных исключений.

Многие стандартные модули определяют свои собственные исключения для сообщения об ошибках, которые могут происходить в функциях, что они определяют. Больше информации о классах представлено в главе Классы.

8.6. Определение очищающих действий

У оператора try есть другой необязательный блок, предназначенный для операций очистки, которые нужно выполнить независимо от условий. Например:

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print('Прощай мир!')
...
Прощай мир!
KeyboardInterrupt
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>

Если присутствует finally блок, finally блок будет выполняться как последняя задача перед завершением try оператором. Блок finally работает, производит ли try оператор исключение или нет. В следующих пунктах рассматриваются более сложные случаи возникновения исключения:

  • Если исключение вызывается во время выполнения предложения try, исключение может обрабатываться предложением except. Если исключение не обрабатывается предложением except, оно повторно создаётся после выполнения блока finally.
  • При выполнении блока except или else может возникнуть исключение. Это исключение вновь вызывается после выполнения блока finally.
  • Если try оператор достигает break, continue или return оператора, то finally уточнение будет выполняться непосредственно перед выполнением break, continue или return операторами.
  • Если finally блок будет включать return оператор, возвращаемое значение будет из return оператора finally, не значение из блока return оператор try.

Например:

>>> def bool_return():
...     try:
...         return True
...     finally:
...         return False
...
>>> bool_return()
False

Более сложный пример:

>>> def divide(x, y):
...     try:
...         result = x / y
...     except ZeroDivisionError:
...         print("деление на ноль!")
...     else:
...         print("результат есть", result)
...     finally:
...         print("выполнение предложения finally")
...
>>> divide(2, 1)
результат есть 2.0
выполнение предложения finally
>>> divide(2, 0)
деление на ноль!
выполнение предложения finally
>>> divide("2", "1")
выполнение предложения finally
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'

Как видно, finally блок выполняется в любом случае. Вызывается TypeError при делении двух строк, не обрабатывается except блоком и поэтому повторно вызывается после выполнения блока finally.

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

8.7. Предопределённые действия по очистке

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

for line in open("myfile.txt"):
    print(line, end="")

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

with open("myfile.txt") as f:
    for line in f:
        print(line, end="")

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