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 only concatenate str (not "int") to str
В последней строке сообщения об ошибке указано, что произошло. Исключения бывают разных типов, и тип выводится как часть сообщения: в примере это типы 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.Если возникает исключение, которое не соответствует исключению, указанному в предложении 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 были изменены на противоположные (сначала было бы указано except B
), то было бы напечатано B, B, B - срабатывает первое соответствующее предложение except.
Когда возникает исключение, оно может иметь связанные значения, также известные как аргументы исключения. Наличие и типы аргументов зависят от типа исключения.
В предложении except после имени исключения может указываться переменная. Переменная привязана к экземпляру исключения, который обычно имеет атрибут args
, в котором хранятся аргументы. Для удобства встроенные типы исключений определяют __str__()
для вывода всех аргументов без явного доступа к .args
.
>>> try:
... raise Exception('spam', 'eggs')
... except Exception as inst:
... print(type(inst)) # the exception type
... 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
Вывод исключения __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 error:", err)
except ValueError:
print("Could not convert data to an integer.")
except Exception as err:
print(f"Unexpected {err=}, {type(err)=}")
raise
Оператор try
… except
содержит необязательное предложение 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
, поскольку это позволяет избежать случайного перехвата исключения, которое не было вызвано кодом, защищенным оператором try
… except
.
Обработчики исключений обрабатывают не только исключения, которые возникают непосредственно в предложении 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, либо класс exception (класс, производный от BaseException
, такой как Exception
, или один из его подклассов). Если передается класс exception, он будет создан неявным образом путем вызова его конструктора без аргументов:
raise ValueError # shorthand for '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. Цепочка исключений¶
Если необработанное исключение возникает внутри раздела except
, к нему будет прикреплено обрабатываемое исключение и включено в сообщение об ошибке:
>>> try:
... open("database.sqlite")
... except OSError:
... raise RuntimeError("unable to handle error")
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
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>
RuntimeError: unable to handle error
Чтобы указать, что одно исключение является прямым следствием другого, оператор raise
допускает необязательное предложение from
:
# exc must be exception instance or 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>
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>
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>
RuntimeError
Для получения дополнительной информации о механизме создания цепочки смотрите раздел Встроенные исключения.
8.6. Исключения, определяемые пользователем¶
Программы могут сами присваивать имена исключениям, создавая новый класс exception (подробнее о классах Python см. в разделе Занятия). Исключения, как правило, должны быть производными от класса Exception
, прямо или косвенно.
Могут быть определены классы исключений, которые делают все, что может сделать любой другой класс, но обычно они остаются простыми, часто предлагая только ряд атрибутов, которые позволяют обработчикам извлекать информацию об ошибке для исключения.
Большинство исключений определяются с именами, заканчивающимися на «Ошибка», аналогично именованию стандартных исключений.
Многие стандартные модули определяют свои собственные исключения, чтобы сообщать об ошибках, которые могут возникать в определенных ими функциях.
8.7. Определение действий по очистке¶
В инструкции 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
.Если в предложении
finally
выполняется операторbreak
,continue
илиreturn
, исключения повторно не вызываются.Если оператор
try
достигает значенияbreak
,continue
илиreturn
, то предложениеfinally
будет выполнено непосредственно передbreak
, выполнение инструкцииcontinue
илиreturn
.Если предложение
finally
содержит инструкциюreturn
, возвращаемое значение будет соответствовать значению из инструкцииfinally
в предложенииreturn
, а не значению из инструкцииtry
утверждение пунктаreturn
.
Например:
>>> 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.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('bad type')
... except Exception as e:
... e.add_note('Add some information')
... e.add_note('Add some more information')
... raise
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
TypeError: bad type
Add some information
Add some more information
>>>
Например, при объединении исключений в группу исключений мы можем захотеть добавить контекстную информацию для отдельных ошибок. Ниже для каждого исключения в группе есть примечание, указывающее, когда произошла эта ошибка.
>>> def f():
... raise OSError('operation failed')
...
>>> excs = []
>>> for i in range(3):
... try:
... f()
... except Exception as e:
... e.add_note(f'Happened in Iteration {i+1}')
... excs.append(e)
...
>>> raise ExceptionGroup('We have some problems', excs)
+ Exception Group Traceback (most recent call last):
| File "<stdin>", line 1, in <module>
| ExceptionGroup: We have some problems (3 sub-exceptions)
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| File "<stdin>", line 2, in f
| OSError: operation failed
| Happened in Iteration 1
+---------------- 2 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| File "<stdin>", line 2, in f
| OSError: operation failed
| Happened in Iteration 2
+---------------- 3 ----------------
| Traceback (most recent call last):
| File "<stdin>", line 3, in <module>
| File "<stdin>", line 2, in f
| OSError: operation failed
| Happened in Iteration 3
+------------------------------------
>>>