Содержание страницы
8. Ошибки и исключения¶Errors and Exceptions
До сих пор мы лишь упоминали сообщения об ошибках, но если вы пробовали примеры, то, скорее всего, уже с ними сталкивались. Существует (как минимум) два различимых вида ошибок: синтаксические ошибки и исключения.
8.1. Синтаксические ошибки¶Syntax Errors
Синтаксические ошибки, также известные как ошибки разбора, – пожалуй, самый распространённый вид неполадок, с которыми вы сталкиваетесь, пока учите Python:
>>> while True print('Hello world')
File "<stdin>", line 1
while True print('Hello world')
^^^^^
SyntaxError: invalid syntax
Парсер повторяет строку с ошибкой и отображает маленькие «стрелочки», указывающие на токен в строке, где была обнаружена ошибка. Ошибка может быть вызвана отсутствием токена перед указанным токеном. В примере ошибка обнаруживается в функции print(), поскольку перед ней отсутствует двоеточие (':'). Имя файла и номер строки выводятся, чтобы было понятно, где искать, если ввод поступил из скрипта.
8.2. Исключения¶Exceptions
Даже если оператор или выражение синтаксически верны, при попытке его выполнения может возникнуть ошибка. Ошибки, обнаруженные во время выполнения, называются исключениями и не являются безусловно фатальными: вы скоро научитесь обрабатывать их в программах на 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. Обработка исключений¶Handling Exceptions
Можно писать программы, обрабатывающие выбранные исключения. В следующем примере показан запрос ввода у пользователя до тех пор, пока не будет введено целое число, но при этом пользователь может прервать программу (с помощью 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)) # тип исключения
... print(inst.args) # аргументы, хранящиеся в .args
... print(inst) # __str__ позволяет выводить args напрямую, но может быть переопределён в подклассах исключений
... # но может быть переопределён в подклассах исключений
... x, y = inst.args # распаковать args
... print('x =', x)
... print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs
Вывод __str__() исключения печатается как последняя часть («подробности»)
сообщения для необработанных исключений.
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.
Exception handlers do not handle only exceptions that occur immediately in the try clause, but also those that occur inside functions that are called (even indirectly) in the try clause. For example:
>>> 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. Возбуждение исключений¶Raising Exceptions
Инструкция raise позволяет программисту принудительно вызвать указанное исключение. Например:
>>> raise NameError('HiThere')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: HiThere
Единственный аргумент raise указывает возбуждаемое исключение. Это должен быть либо экземпляр исключения, либо класс исключения (класс, производный от BaseException, например Exception или один из его подклассов). Если передан класс исключения, он будет неявно создан вызовом его конструктора без аргументов:
raise ValueError # сокращение для '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. Цепочка исключений¶Exception Chaining
Если необработанное исключение возникает внутри раздела 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 должен быть экземпляром исключения или 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. Определяемые пользователем исключения¶User-defined Exceptions
Программы могут задавать собственные исключения, создавая новый класс исключения (см. Классы для получения дополнительной информации о классах Python). Исключения обычно должны быть производными от класса Exception, прямо или косвенно.
Классы исключений могут быть определены так, чтобы делать всё, что может любой другой класс, но обычно их оставляют простыми, часто предоставляя лишь несколько атрибутов, позволяющих обработчикам извлекать информацию об ошибке.
Большинство исключений имеют имена, оканчивающиеся на «Error», подобно именованию стандартных исключений.
Многие стандартные модули определяют собственные исключения для сообщения об ошибках, которые могут возникнуть в определяемых ими функциях.
8.7. Определение действий по очистке¶Defining Clean-up Actions
Инструкция 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, возвращаемое значение будет взято из оператора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("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. Предопределённые действия по очистке¶Predefined Clean-up Actions
Некоторые объекты определяют стандартные действия по очистке, которые должны быть выполнены, когда объект больше не нужен, независимо от успеха или неудачи операции с этим объектом. Рассмотрим следующий пример, в котором предпринимается попытка открыть файл и вывести его содержимое на экран.
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. Обогащение исключений заметками¶Enriching Exceptions with Notes
Когда исключение создаётся для возбуждения, оно обычно инициализируется
информацией, описывающей произошедшую ошибку. Бывают случаи,
когда полезно добавить информацию после перехвата исключения. Для этого
у исключений есть метод 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
+------------------------------------
>>>