Spec-Zone.ru › Python 3.12

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

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

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 возникает исключение, остальная часть блока 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, потому что это предотвращает случайное перехват исключения, которое не было вызвано кодом, защищённым оператором else … 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–2024 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.12/tutorial/errors.html

Spec-Zone.ru

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