Spec-Zone.ru › Python 3.11

Ошибки и исключения

До сих пор сообщения об ошибках упоминались лишь вскользь, но если вы пробовали примеры, то, вероятно, видели некоторые из них. Существует (по крайней мере) два различимых типа ошибок: синтаксические ошибки и исключения.

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. Обработка исключений

Возможно создание программ, которые обрабатывают выбранные исключения. Рассмотрим следующий пример, который запрашивает у пользователя ввод, пока не будет введён целое число, но позволяет пользователю прервать программу (с помощью Ctrl-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__() печатается как последняя часть («детали») сообщения для необработанных исключений.

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

© 2001–2023 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.11/tutorial/errors.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API