Spec-Zone.ru › Python 3.13

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

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

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>
    10 * (1/0)
          ~^~
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    4 + spam*3
        ^^^^
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    '2' + 2
    ~~~~^~~
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__() исключения печатается как последняя часть (‘подробности’) сообщения для необработанных исключений.

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>
    raise NameError('HiThere')
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>
    raise NameError('HiThere')
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>
    open("database.sqlite")
    ~~~~^^^^^^^^^^^^^^^^^^^
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>
    raise RuntimeError("unable to handle error")
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>
    func()
    ~~~~^^
  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>
    raise RuntimeError('Failed to open database') from exc
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>
    raise RuntimeError from None
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>
    raise KeyboardInterrupt
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>
    divide("2", "1")
    ~~~~~~^^^^^^^^^^
  File "<stdin>", line 3, in divide
    result = x / y
             ~~^~~
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>
  |     f()
  |     ~^^
  |   File "<stdin>", line 3, in f
  |     raise ExceptionGroup('there were problems', excs)
  | ExceptionGroup: there were problems (2 sub-exceptions)
  +-+---------------- 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>
  |     f()
  |     ~^^
  |   File "<stdin>", line 2, in f
  |     raise ExceptionGroup(
  |     ...<12 lines>...
  |     )
  | ExceptionGroup: group1 (1 sub-exception)
  +-+---------------- 1 ----------------
    | ExceptionGroup: group2 (1 sub-exception)
    +-+---------------- 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>
    raise TypeError('bad type')
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>
  |     raise ExceptionGroup('We have some problems', excs)
  | ExceptionGroup: We have some problems (3 sub-exceptions)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 1
    +---------------- 2 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 2
    +---------------- 3 ----------------
    | Traceback (most recent call last):
    |   File "<stdin>", line 3, in <module>
    |     f()
    |     ~^^
    |   File "<stdin>", line 2, in f
    |     raise OSError('operation failed')
    | OSError: operation failed
    | Happened in Iteration 3
    +------------------------------------
>>>

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

Spec-Zone.ru

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