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

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

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

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

>>> while True print('Hello world')
  File "<stdin>", line 1
    while True print('Hello world')
                   ^
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("Please enter a number: "))
...         break
...     except ValueError:
...         print("Oops!  That was no valid number.  Try again...")
...

Инструкция 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("OS error: {0}".format(err))
except ValueError:
    print("Could not convert data to an integer.")
except:
    print("Unexpected error:", sys.exc_info()[0])
    raise

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

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except OSError:
        print('cannot open', arg)
    else:
        print(arg, 'has', len(f.readlines()), 'lines')
        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('Handling run-time error:', err)
...
Handling run-time error: division by zero

8.4. Подъем исключений

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

>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: HiThere

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

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

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

>>> try:
...     raise NameError('HiThere')
... except NameError:
...     print('An exception flew by!')
...     raise
...
An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
NameError: HiThere

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('Goodbye, world!')
...
Goodbye, world!
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("division by zero!")
...     else:
...         print("result is", result)
...     finally:
...         print("executing finally clause")
...
>>> divide(2, 1)
result is 2.0
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
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 всегда закрывается, даже если при прочтении строк обнаружилась проблема. В документации к объектам, которые поддерживают предопределённые действия по подчистке, таким как файлы, эта их способность будет явно указана.