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

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

Обработчики исключений обрабатывают не только исключения, которые возникают непосредственно в предложении 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.9. Создание и обработка нескольких несвязанных исключений

Бывают ситуации, когда необходимо сообщить о нескольких возникших исключениях. Это часто происходит в платформах параллелизма, когда несколько задач могут завершиться сбоем параллельно, но есть и другие варианты использования, когда желательно продолжить выполнение и собрать несколько ошибок, а не вызывать первое исключение.

Встроенный ExceptionGroup содержит список экземпляров исключений, так что они могут быть вызваны вместе. Это само по себе исключение, поэтому его можно перехватить, как и любое другое исключение.

>>> def f():
...     excs = [OSError('error 1'), SystemError('error 2')]
...     raise ExceptionGroup('there were problems', excs)
...
>>> f()
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 1, in <module>
  |   File "<stdin>", line 3, in f
  | ExceptionGroup: there were problems
  +-+---------------- 1 ----------------
    | OSError: error 1
    +---------------- 2 ----------------
    | SystemError: error 2
    +------------------------------------
>>> try:
...     f()
... except Exception as e:
...     print(f'caught {type(e)}: e')
...
caught <class 'ExceptionGroup'>: e
>>>

Используя except* вместо except, мы можем выборочно обрабатывать только те исключения в группе, которые соответствуют определенному типу. В следующем примере, который показывает вложенную группу исключений, каждое предложение except* извлекает из группы исключения определенного типа, позволяя всем остальным исключениям распространяться на другие предложения и в конечном итоге повторяться.

>>> def f():
...     raise ExceptionGroup(
...         "group1",
...         [
...             OSError(1),
...             SystemError(2),
...             ExceptionGroup(
...                 "group2",
...                 [
...                     OSError(3),
...                     RecursionError(4)
...                 ]
...             )
...         ]
...     )
...
>>> try:
...     f()
... except* OSError as e:
...     print("There were OSErrors")
... except* SystemError as e:
...     print("There were SystemErrors")
...
There were OSErrors
There were SystemErrors
  + Exception Group Traceback (most recent call last):
  |   File "<stdin>", line 2, in <module>
  |   File "<stdin>", line 2, in f
  | ExceptionGroup: group1
  +-+---------------- 1 ----------------
    | ExceptionGroup: group2
    +-+---------------- 1 ----------------
      | RecursionError: 4
      +------------------------------------
>>>

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

>>> excs = []
... for test in tests:
...     try:
...         test.run()
...     except Exception as e:
...         excs.append(e)
...
>>> if excs:
...    raise ExceptionGroup("Test Failures", excs)
...

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
    +------------------------------------
>>>
Вернуться на верх