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 clause, оно передается во внешние операторы
try
; если обработчик не найден, это необработанное исключение, и выполнение останавливается с сообщением об ошибке.
В операторе try
может быть более одного предложения except, чтобы указать обработчики для разных исключений. Будет выполнен только один обработчик. Обработчики обрабатывают только те исключения, которые встречаются в соответствующем предложении try clause, а не в других обработчиках того же оператора 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 clauses были перевернуты (с except B
первым), то было бы выведено B, B, B - срабатывает первый подходящий пункт except clause.
Когда возникает исключение, оно может иметь связанные с ним значения, также известные как аргументы исключения. Наличие и типы аргументов зависят от типа исключения.
В предложении 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 clause, но и те, которые возникают внутри функций, вызываемых (даже косвенно) в предложении try clause. Например:
>>> 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
указывает на исключение, которое должно быть поднято. Это должен быть либо экземпляр исключения, либо класс исключения (класс, производный от BaseException
, например 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. Определяемые пользователем исключения¶
Программы могут называть свои собственные исключения, создавая новый класс исключений (подробнее о классах Python см. в разделе Занятия). Как правило, исключения должны быть производными от класса Exception
, прямо или косвенно.
Можно определить классы исключений, которые делают все, что может сделать любой другой класс, но обычно они остаются простыми, предлагая только ряд атрибутов, которые позволяют извлекать информацию об ошибке обработчиками исключений.
Большинство исключений имеют имена, заканчивающиеся на «Error», аналогично именованию стандартных исключений.
Многие стандартные модули определяют свои собственные исключения для сообщения об ошибках, которые могут возникнуть в определенных ими функциях.
8.7. Определение действий по очистке¶
Оператор try
имеет еще один необязательный пункт, который предназначен для определения действий по очистке, которые должны быть выполнены при любых обстоятельствах. Например:
>>> try:
... raise KeyboardInterrupt
... finally:
... print('Goodbye, world!')
...
Goodbye, world!
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
KeyboardInterrupt
Если присутствует предложение 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
+------------------------------------
>>>