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

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

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

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

>>> while True print('Hello world')
  File "<stdin>", line 1, in ?
    while True print('Hello world')
                   ^
SyntaxError: invalid syntax

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

Исключения

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

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

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

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

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

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

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

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

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...         break
...     except ValueError:
...         print("Oops!  Это некорректное число.  Try again...")
...

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

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

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

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

В последнем блоке 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

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

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except IOError:
        print('cannot open', arg)
    else:
        print(arg, 'has', len(f.readlines()), 'lines')
        f.close()

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

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

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

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))    # the exception instance
...     print(inst.args)     # arguments stored in .args
...     print(inst)          # __str__ allows args to be printed directly,
...                          # but may be overridden in exception subclasses
...     x, y = inst.args     # unpack 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: int division or modulo by zero

Порождение исключений

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

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

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

Если вам нужно определить, было ли возбуждено исключение, не перехватывая его — упрощённая форма оператора 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 ?
NameError: HiThere

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

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

>>> class MyError(Exception):
...     def __init__(self, value):
...         self.value = value
...     def __str__(self):
...         return repr(self.value)
...
>>> try:
...     raise MyError(2*2)
... except MyError as e:
...     print('My exception occurred, value:', e.value)
...
My exception occurred, value: 4
>>> raise MyError('oops!')
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
__main__.MyError: 'oops!'

В этом примере был перегружен конструктор по умолчанию __init__() класса Exception. Новое поведение отличается лишь созданием нового атрибута value. Это заменяет поведение по умолчанию, при котором создаётся атрибут args.

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

class Error(Exception):
    """Base class for exceptions in this module."""
    pass

class InputError(Error):
    """Exception raised for errors in the input.

    Attributes:
        expression -- input expression in which the error occurred
        message -- explanation of the error
    """

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

class TransitionError(Error):
    """Raised when an operation attempts a state transition that's not
    allowed.

    Attributes:
        previous -- state at beginning of transition
        next -- attempted new state
        message -- explanation of why the specific transition is not allowed
    """

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

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

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

Определение действий при подчистке

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

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print('Goodbye, world!')
...
Goodbye, world!
Traceback (most recent call last):
  File "<stdin>", line 2, in ?
KeyboardInterrupt

Блок finally всегда выполняется перед выходом из оператора try, происходит ли исключение или нет. Если в блоке try появилось исключение, которое не было обработано в блоке except (или появилось в самих блоках except или else) — оно порождается заново после выполнения блока finally. Также блок finally исполняется «по пути наружу», если какой-либо другой блок оператора try был покинут за счёт одного из операторов: break, continue или return. Более сложный пример:

>>> 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 ?
  File "<stdin>", line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'

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

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

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

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

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

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

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

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