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
У оператора try
… except
есть необязательный блок 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
, поскольку исключает неожиданный перехват исключения, которое
появилось не по причине выполнения кода, защищенного оператором try
… except
.
При появлении исключения, оно может иметь ассоциированное значение, также известное как аргумент исключения. Присутствие и тип аргумента зависят от типа самого исключения.
В блоке 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 всегда закрывается, даже если при прочтении строк обнаружилась проблема. В документации к объектам, которые поддерживают предопределённые действия по очистке, таким как файлы, данная их способность будет явно указана.