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 и except).

  • Если исключение не происходит, то except clause пропускается и выполнение оператора try завершается.

  • Если во время выполнения предложения try возникает исключение, остальная часть предложения пропускается. Затем, если его тип совпадает с исключением, названным после ключевого слова except, выполняется предложение except clause, а затем выполнение продолжается после блока try/except.

  • Если возникает исключение, которое не соответствует исключению, названному в except clause, оно передается во внешние операторы try; если обработчик не найден, это не обработанное исключение, и выполнение останавливается с сообщением, как показано выше.

Оператор try может содержать более одного except clause, чтобы указать обработчики для различных исключений. Будет выполнен только один обработчик. Обработчики обрабатывают только те исключения, которые встречаются в соответствующем предложении try clause, а не в других обработчиках того же оператора try. В предложении except clause несколько исключений могут быть названы в виде кортежа со скобками, например:

... 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.

Все исключения наследуются от BaseException, и поэтому его можно использовать в качестве подстановочного знака. Используйте его с особой осторожностью, так как таким образом легко замаскировать реальную ошибку программирования! Его также можно использовать для печати сообщения об ошибке, а затем повторно поднять исключение (позволяя вызывающей стороне также обработать исключение):

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("OS error: {0}".format(err))
except ValueError:
    print("Could not convert data to an integer.")
except BaseException as err:
    print(f"Unexpected {err=}, {type(err)=}")
    raise

В качестве альтернативы в последнем предложении except можно опустить имя (имена) исключения, однако тогда значение исключения должно быть получено из sys.exc_info()[1].

Оператор tryexcept имеет необязательное предложение 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.

Когда возникает исключение, оно может иметь связанное с ним значение, также известное как аргумент исключения. Наличие и тип аргумента зависят от типа исключения.

В предложении except clause после имени исключения может быть указана переменная. Переменная связана с экземпляром исключения, аргументы которого хранятся в instance.args. Для удобства экземпляр исключения определяет __str__(), поэтому аргументы могут быть выведены напрямую без необходимости ссылаться на .args. Можно также сначала инстанцировать исключение перед его созданием и добавить к нему любые атрибуты по желанию.

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))    # the exception instance
...     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

Если исключение имеет аргументы, они выводятся как последняя часть («detail») сообщения для необработанных исключений.

Обработчики исключений обрабатывают исключения не только в том случае, если они возникают непосредственно в предложении 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 указывает на исключение, которое должно быть вызвано. Это должен быть либо экземпляр исключения, либо класс исключения (класс, производный от 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. Цепочка исключений

Оператор 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

Цепочка исключений происходит автоматически, когда исключение возникает внутри секции except или finally. Это можно отключить, используя идиому 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!
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 всегда закрывается, даже если при обработке строк возникла проблема. Объекты, которые, как и файлы, предоставляют предопределенные действия по очистке, будут указывать на это в своей документации.

Вернуться на верх