8. Ошибки и исключения¶
До настоящего момента сообщения об ошибках упоминались лишь вскользь, но если вы пробовали запускать примеры с кодом, вы, вероятно, их встречали. Существует (по крайней мере) два различных типа ошибок: синтаксические ошибки и исключения
8.1. Синтаксические ошибки¶
Пока вы только изучаете Python, чаще всего вам будут попадаться синтаксически ошибки, или ошибки разбора (парсинга):
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^^^^^
SyntaxError: invalid syntax
Анализатор повторяет строку с ошибкой и отображает маленькие стрелки, указывающие на место, где была обнаружена ошибка. Обратите внимание, что это не всегда то место, которое нужно исправить. В примере ошибка обнаружена в функции print(), поскольку двоеточие (':') отсутствует непосредственно перед ней.
Имя файла (<stdin> в нашем примере) и номер строки выводятся для того, чтобы вы знали, где искать ошибку, если ввод поступал из файла.
8.2. Исключения¶
Даже если инструкция или выражение корректны синтаксически, они могут привести к ошибке при попытке их исполнить. Ошибки, появляющиеся во время выполнения программы, называются исключениями, и они не всегда фатальны: скоро вы узнаете, как их обрабатывать. Однако большинство исключений не обрабатываются программами и приводят к сообщениями об ошибке, как это:
>>> 10 * (1/0)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
10 * (1/0)
~^~
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
4 + spam*3
^^^^
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
'2' + 2
~~~~^~~
TypeError: can only concatenate str (not "int") to str
Последняя строка сообщения об ошибке указывает, что произошло. Исключения бывают разных типов, и тип исключения выводится как часть сообщения: в примере появились ошибки типов ZeroDivisionError, NameError и TypeError. Строка, напечатанная в качестве типа исключения — это имя встроенного исключения, которое произошло. Это верно для всех встроенных исключений, но может быть неверно для пользовательских (хотя такое соответствие — полезная практика). Имена стандартных исключений — встроенные идентификаторы (не зарезервированные ключевые слова).
Остаток строки содержит уточняющую информацию, которая зависит от типа исключения и от причины его возникновения.
Начальная часть сообщения об ошибке показывает контекст, в котором произошло исключение, в виде трассировки стека. Обычно в ней содержится трассировка со строками исходного кода; однако она не показывает строки, прочитанные из стандартного ввода.
В разделе Built-in Exceptions перечислены встроенные исключения и их смысл.
8.3. Обработка исключений¶
В Python можно писать программы, которые обрабатывают выбранные исключения. Посмотрите на следующий пример, который запрашивает у пользователя ввод до тех пор, пока не будет введено правильное целое число, но при этом позволяет пользователю прервать программу (с помощью Control-C или другого способа, поддерживаемого операционной системой). Обратите внимание, что прерывание, инициированное пользователем, сигнализируется с помощью исключения KeyboardInterrupt.
>>> while True:
... try:
... x = int(input("Пожалуйста, введите число: "))
... break
... except ValueError:
... print("Упс! Это было невалидное число. Попробуйте снова...")
...
Инструкция try работает следующим образом.
Первым делом, выполняется ветка try, то есть инструкции между ключевыми словами
tryиexcept.Если исключение не возникает, ветка except пропускается, и выполнение инструкции
tryзавершается.Если исключение возникает во время выполнения ветки
try, оставшаяся её часть пропускается. Затем, если его тип совпадает с именем исключения, указанным после ключевого словаexcept, выполняется ветка except, а затем выполнение продолжается после блока try/except.Если возникает исключение, которое не совпадает с именем исключения в ветке except, оно передается внешним инструкциям
try; если обработчик не найден, это необработанное исключение, и выполнение останавливается с сообщением об ошибке.
Инструкция try может содержать более одной ветви except, чтобы указать обработчики для разных исключений. Выполнится не более одного обработчика. Обработчики обрабатывают только исключения, возникающие в соответствующей ветке try, а не в других обработчиках той же инструкции try. Ветка except может указывать несколько исключений, например:
... except RuntimeError, TypeError, NameError:
... pass
Класс в ветке except соответствует исключениям, которые являются экземплярами самого класса или одного из его производных классов (но не наоборот — ветка 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 B на первое место), код напечатает B, B, B — сработает первай совпадающая ветка except.
Когда возникает исключение, оно может иметь связанные значения, также известные как аргументы исключения. Наличие и типы аргументов зависят от типа исключения.
Ветка except может указать переменную после имени исключения. Переменная привязывается к экземпляру исключения, который обычно имеет атрибут args, в котором хранятся аргументы. Для удобства, встроенные типы исключений определяют метод __str__(), чтобы печатать все аргументы без явного доступа к .args.
>>> try:
... raise Exception('спам', 'яйца')
... except Exception as inst:
... print(type(inst)) # тип исключения
... print(inst.args) # аргументы хранятся в .args
... print(inst) # __str__ позволяет вывести args напрямую,
... # но может быть переопределён в подклассах исключений
... x, y = inst.args # распаковка args
... print('x =', x)
... print('y =', y)
...
<class 'Exception'>
('спам', 'яйца')
('спам', 'яйца')
x = спам
y = яйца
Вывод __str__() исключения печатается в виде последней части („detail“) сообщения для необработанных исключений.
BaseException — это общий базовый класс всех исключений. Одним из его подклассов является Exception, базовый класс всех некритических исключений. Исключения, которые не являются подклассами Exception, обычно не обрабатываются, потому что они используются для указания на то, что программа должна завершиться. В частности, это SystemExit, которое возбуждается методом sys.exit(), и KeyboardInterrupt, которое возбуждается, когда пользователь хочет прервать программу.
Exception можно использовать в качестве подстановочного знака, который перехватывает (почти) всё. Однако хорошей практикой является быть максимально конкретным в отношении типов исключений, которые мы собираемся обрабатывать, и разрешать любым неожиданным исключениям распространяться дальше.
Самый распространенный шаблон для обработки Exception — это печать или логирование исключения, а затем повторное его возбуждение (позволяя вызывающей стороне также обработать его):
import sys
try:
f = open('myfile.txt')
s = f.readline()
i = int(s.strip())
except OSError as err:
print("Ошибка OS:", err)
except ValueError:
print("Не удалось преобразовать данные в целое число.")
except Exception as err:
print(f"Неожиданная ошибка {err=}, {type(err)=}")
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.
Обработчики исключений обрабатывают не только исключения, которые возникают непосредственно в ветке 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>
raise NameError('ПриветТам')
NameError: ПриветТам
Единственный аргумент, принимаемый raise, указывает на исключение, которое нужно породить. Тут можно указать либо экземпляр исключения, либо класс исключения (класс, который наследуется от BaseException, такой как Exception или один из его подклассов). Если передаётся класс исключения, экземпляр будет неявно создан с помощью вызова конструктора класса без аргументов:
raise ValueError # сокращение для 'raise ValueError()'
Если вы хотите узнать, возникло исключение или нет, но не планируете его обрабатывать, есть более простая форма инструкции raise. Она позволяет повторно выбросить исключение:
>>> try:
... raise NameError('ПриветТам')
... except NameError:
... print('Мимо пролетело исключение!')
... raise
...
Мимо пролетело исключение!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise NameError('ПриветТам')
NameError: ПриветТам
8.5. Цепочка исключений¶
Если внутри секции except возникает необработанное исключение, оно будет иметь прикрепленное к нему обрабатываемое исключение и будет включено в сообщение об ошибке:
>>> try:
... open("database.sqlite")
... except OSError:
... raise RuntimeError("unable to handle error")
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
open("database.sqlite")
~~~~^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError("unable to handle error")
RuntimeError: unable to handle error
Для указания того, что исключение является прямым следствием другого, инструкция raise позволяет использовать необязательное ключевое слово from:
# exc должен быть экземпляром исключения или None.
raise RuntimeError from exc
Это может быть полезно при преобразовании одного исключения в другое. Например:
>>> def func():
... raise ConnectionError
...
>>> try:
... func()
... except ConnectionError as exc:
... raise RuntimeError('Failed to open database') from exc
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
func()
~~~~^^
File "<stdin>", line 2, in func
ConnectionError
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError('Failed to open database') from exc
RuntimeError: Failed to open database
Оно также позволяет отключить автоматическое связывание исключений с использованием идиомы from None:
>>> try:
... open('database.sqlite')
... except OSError:
... raise RuntimeError from None
...
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
raise RuntimeError from None
RuntimeError
Для получения дополнительной информации о механизмах связывания, см. Built-in Exceptions.
8.6. Пользовательские исключения¶
Программы могут создавать собственные исключения, определив новый класс исключения (см. Классы, чтобы узнать больше про классы в Python). Исключения обычно наследуются от класса Exception напрямую или косвенно.
Классы исключений могут быть определены таким образом, чтобы делать всё то же самое, что и любой другой класс. Но обычно они остаются простыми, часто предлагая только несколько атрибутов, которые позволяют обработчикам извлекать информацию об ошибке.
Большинство исключений определены с именем, оканчивающимся на «Error», подобно стандартным исключениям.
Многие стандартные модули определяют свои собственные исключения для сообщения об ошибках, которые могут возникнуть в функциях этого модуля.
8.7. Определение действий по очистке¶
Инструкция try может содержать ещё одну необязательную ветку, предназначенную для определения действий по очистке, которые должны быть выполнены при любых обстоятельствах. Например:
>>> try:
... raise KeyboardInterrupt
... finally:
... print('До свидания, мир!')
...
До свидания, мир!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise KeyboardInterrupt
KeyboardInterrupt
Если ветка finally присутствует, код в ней выполнится в последнюю очередь перед заверешением инструкции try. Ветка finally выполняется независимо от того, произошло исключение или нет. Следующие пункты описывают более сложные случаи возникновения исключений:
Если исключение происходит в ветка
try, оно может быть обработано в веткеexcept. Если исключение не было обработано в веткеexcept, оно повторно выбрасывается после выполнения веткиfinally.Исключение может произойти в ветках
exceptилиelse. Как и в предыдущем случае, исключение порождается заново после выполнения веткиfinally.Если в ветке
finallyвыполняются инструкцииbreak,continueилиreturn, то исключение повторно не выбрасывается. Это может запутать и поэтому не рекомендуется. Начиная с версия 3.14 компилятор выдаёт для этого случаяSyntaxWarning(см. PEP 765).Если внутри
tryвыполняется инструкцияbreak,continueилиreturn, веткаfinallyвыполнится прямо перед выполнением инструкцииbreak,continueилиreturn.Если ветка
finallyсодержит инструкцииreturn, то возвращаемое значение будет браться из инструкцийreturnветкиfinally, а не из инструкцийreturnветкиtry. Это может запутать, поэтому такая практика не рекомендуется. Начиная с версия 3.14 компилятор выдаёт для этого случаяSyntaxWarning(см. PEP 765).
Например:
>>> 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>
divide("2", "1")
~~~~~~^^^^^^^^^^
File "<stdin>", line 3, in divide
result = x / y
~~^~~
TypeError: unsupported operand type(s) for /: 'str' and 'str'
Как можно заметить, ветка finally выполняется при любом исходе. Исключение TypeError, возникшее при делении двух строк, не обрабатывается блоком except и выбрасывается заново после выполнения блока finally.
В реальных приложениях блок finally может быть полезен для освобождения внешних ресурсов (например, файлов или сетевых соединений) независимо от того, было их использование успешным или нет.
8.8. Предопределённые действие по очистке¶
Некоторые объекты определяют стандартные действия по очистке, которые выполняются, когда объект больше не нужен, независимо от того, была ли операция с объектом успешной или нет. Рассмотрим следующий пример, который пытается открыть файл и вывести его содержимое на экран.
for line in open("myfile.txt"):
print(line, end="")
Проблема этого кода в том, что файл остаётся открытым неопределённое время после завершения этой части кода. В небольших скриптах это не вызывает проблем, но в крупных приложениях может привести к неприятностям. Инструкция with позволяет использовать объекты таким образом, чтобы они всегда очищали свои ресурсы незамедлительно и корректно.
with open("myfile.txt") as f:
for line in f:
print(line, end="")
После выполнения этой инструкции файл f всегда будет закрыт, даже если при обработке строк возникла ошибка. Объекты, которые, как файлы, предоставляют предопределённые действия по очистке, указывают это в своей документации.
8.10. Дополнение исключений заметками¶
Когда исключение создается для того, чтобы быть выброшенным, оно обычно инициализируется информацией, которая описывает возникшую ошибку. Есть случаи, когда полезно добавить информацию после того, как исключение было перехвачено. Для этой цели у исключений есть метод add_note(note), который принимает строку и добавляет её в список заметок исключения. Стандартное отображение трассировки стека включает все заметки в порядке их добавления, после исключения.
>>> try:
... raise TypeError('неверный тип')
... except Exception as e:
... e.add_note('Добавлена дополнительная информация')
... e.add_note('Добавлено больше дополнительной информации')
... raise
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
raise TypeError('bad type')
TypeError: неверный тип
Добавлена дополнительная информация
Добавлено больше дополнительной информации
>>>
Например, при сборе исключений в группу исключений мы можем добавить контекстную информацию для отдельных ошибок. В следующем примере каждое исключение в группе имеет заметку, указывающую, когда произошла эта ошибка.
>>> def f():
... raise OSError('операция не выполнена')
...
>>> excs = []
>>> for i in range(3):
... try:
... f()
... except Exception as e:
... e.add_note(f'Произошло в итерации {i+1}')
... excs.append(e)
...
>>> raise ExceptionGroup('Возникли проблемы', excs)
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| raise ExceptionGroup('Возникли проблемы', excs)
| ExceptionGroup: We have some problems (3 sub-exceptions)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('операция не выполнена')
| OSError: операция не выполнена
| Произошло в итерации 1
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('операция не выполнена')
| OSError: операция не выполнена
| Произошло в итерации 2
+---------------- 3 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| f()
| ~^^
| File "<stdin>", line 2, in f
| raise OSError('операция не выполнена')
| OSError: операция не выполнена
| Произошло в итерации 3
+------------------------------------
>>>