Сообщить об ошибке.

Перехват группы исключений оператором except* в Python

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

  • Одновременно может произойти сбой нескольких параллельных задач.
  • Код очистки может вызвать собственные ошибки.
  • Сбой нескольких пользовательских обратных вызовов.
  • Множественные ошибки в сложном вычислении.
  • Ошибки в коде-обертке.
  • Код при возникновении ошибки может вызвать несколько различных альтернатив, которые в свою очередь также вызывают исключения.

Общие сведения об операторе try/except* со звездочкой.

Новое в Python 3.11

Изменено в Python 3.12: Когда конструкция try-except* обрабатывает всю группу ExceptionGroup и вызывает еще одно исключение, это исключение больше не помещается в ExceptionGroup. Это поведение было изменено в версии 3.11.4. но не задокументировано (Предоставлено Ирит Катриэль.)

Специальный оператор try/except* со звездочкой (новое в Python 3.11) используются для обработки классов групп исключений ExceptionGroup и BaseExceptionGroup (новое в Python 3.11), которые соответствуют их подгруппам на основе типов содержащихся исключений.

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

Тип исключения для сопоставления интерпретируется так же, как и в случае с try/except, но в случае групп исключений можно иметь частичное совпадение, когда тип соответствует некоторым исключениям в группе. Это означает, что можно использовать несколько специальных операторов except*, каждое из которых будет обрабатывать часть группы исключений. Каждое предложение/оператор except* со звёздочкой выполняется один раз и обрабатывает группу исключений из всех соответствующих исключений. Каждое исключение в группе обрабатывается не более чем одним предложением except*, первым, которое ему соответствует.

Если ничего не понятно, то читайте ниже раздел "Понимание того, как работает except* со звездочкой".

try:
    raise ExceptionGroup("eg",
        [ValueError(1), TypeError(2), OSError(3), OSError(4)])
except* TypeError as e:
    print(f'caught {type(e)} with nested {e.exceptions}')
except* OSError as e:
    print(f'caught {type(e)} with nested {e.exceptions}')

# caught <class 'ExceptionGroup'> with nested (TypeError(2),)
# caught <class 'ExceptionGroup'> with nested (OSError(3), OSError(4))
#   + Exception Group Traceback (most recent call last):
#   |   File "<stdin>", line 2, in <module>
#   | ExceptionGroup: eg
#   +-+---------------- 1 ----------------
#     | ValueError: 1
#     +------------------------------------

ВНИМАНИЕ! Нельзя смешивать операторы except (без звездочки) и except* (со звездочкой) в одном и том же блоке try. Операторы break, continue и return не могут использоваться в предложении except*.

Любые оставшиеся исключения, которые не были обработаны каким-либо except* (со звездочкой) повторно вызываются в конце и объединяются в группу исключений вместе со всеми исключениями, которые были вызваны в предложениях except*.

Специальный оператор except* (со звездочкой) должен иметь соответствующий тип, и этот тип не может быть подклассом BaseExceptionGroup.

Для обработки нескольких исключений из ExceptionGroup в одном предложении, также нужно использовать специальный оператор except* (со звездочкой), как показано ниже:

try:
    raise ExceptionGroup('Example ExceptionGroup', (
        TypeError('Example TypeError'),
        ValueError('Example ValueError'),
        KeyError('Example KeyError'),
        AttributeError('Example AttributeError')
    ))
except* TypeError:
    ...

except* ValueError as e:
    ...

# нескольких исключений в одном предложении
except* (KeyError, AttributeError) as e:
    ...

С этой новой функциональностью, возможности обработки исключений - безграничны.

Понимание, как работает except* со звездочкой.

Исключения определяются в иерархии. Например, ModuleNotFoundError - наследуется от ImportError, который в свою очередь наследуется от Exception.

Примечание. Так как большинство исключений наследуются от Exception, можно конечно попытаться упростить обработку ошибок, используя только блоки с Exception. Это плохая идея. Необходимо чтобы блоки исключений были как можно более конкретными, во избежании возникновения непредвиденных ошибок.

При использовании обычного except без звёздочки, первый соответствующий ошибке except вызовет обработку исключения:

>>> try:
...     import no_such_module
... except ImportError as err:
...     print(f"ImportError: {err.__class__}")
... except ModuleNotFoundError as err:
...     print(f"ModuleNotFoundError: {err.__class__}")

# ImportError: <class 'ModuleNotFoundError'>

Когда импортируется несуществующий модуль, то Python3 вызовет исключение ModuleNotFoundError. Но так как класс исключения ModuleNotFoundError наследуется от ImportError, то обработчик ошибок инициирует предложение except ImportError as err:, которое расположено первым.

Обратите внимание, что:

  • Срабатывает не более одного оператора except,
  • Сработает первое совпавшее предложение except.

Если кто-то раньше имел дело с исключениями, то такое поведение кажется интуитивно понятным. Однако группы исключений ведут себя по-другому.

Хотя при использовании обычного except одновременно активно не более одного исключения, можно связать связанные исключения в цепочку. Эта цепочка была введена для Python 3.0. В качестве примера смотрим, что произойдет, если вызвать новое исключение при обработке ошибки:

>>> try:
...     "3" + 11
... except TypeError:
...     raise ValueError(654) # вызов нового исключения

# Traceback (most recent call last):
#   ...
# TypeError: can only concatenate str (not "int") to str
# 
# During handling of the above exception, another exception occurred:
# 
# Traceback (most recent call last):
#   ...
# ValueError: 654

Обратите внимание, что во время обработки исключения TypeError произошло другое исключение. Предложение except TypeError: вызвало трассировку, представляющую исходную ошибку, вызванную кодом. Затем есть еще одна трассировка, представляющая новую ошибку ValueError, которая вызывается принудительно при помощи raise при обработке ошибки TypeError.

Можно использовать цепочку исключений для создания нескольких исключений одновременно. Обратите внимание, что этот механизм предназначен для связанных исключений, особенно когда одно исключение возникает во время обработки другого. Такое поведение отличается от варианта использования группы исключений. Группы исключений объединяют несвязанные исключения в том смысле, что они возникают независимо друг от друга. При обработке связанных исключений можно поймать и обработать только последнюю ошибку в цепочке.

В отличие от большинства других исключений, группы исключений при инициализации принимают два аргумента:

  • Обычное описание,
  • Последовательность подисключений.

Последовательность подисключений может включать другие группы исключений, но не может быть пустой:

>>> ExceptionGroup("one error", [ValueError(654)])
# ExceptionGroup('one error', [ValueError(654)])

>>> ExceptionGroup("two errors", [ValueError(654), TypeError("int")])
# ExceptionGroup('two errors', [ValueError(654), TypeError('int')])

>>> ExceptionGroup("nested",
...     [
...         ValueError(654),
...         ExceptionGroup("imports",
...             [
...                 ImportError("no_such_module"),
...                 ModuleNotFoundError("another_module"),
...             ]
...         ),
...     ]
... )

>>> ExceptionGroup('nested', [ValueError(654), ExceptionGroup('imports',
# [ImportError('no_such_module'), ModuleNotFoundError('another_module')])])

# Последовательность подисключений 
# не может быть пустой
>>> ExceptionGroup("no errors", [])
# Traceback (most recent call last):
#   ...
# ValueError: second argument (exceptions) must be a non-empty sequence

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

Трассировка группы исключений отформатирована так, чтобы четко показать структуру внутри группы. Увидеть трассировку можно, когда происходит вызов группы исключений:

>>> raise ExceptionGroup("nested",
...     [
...         ValueError(654),
...         ExceptionGroup("imports",
...             [
...                 ImportError("no_such_module"),
...                 ModuleNotFoundError("another_module"),
...             ]
...         ),
...         TypeError("int"),
...     ]
... )

#   + Exception Group Traceback (most recent call last):
#   |   ...
#   | ExceptionGroup: nested (3 sub-exceptions)
#   +-+---------------- 1 ----------------
#     | ValueError: 654
#     +---------------- 2 ----------------
#     | ExceptionGroup: imports (2 sub-exceptions)
#     +-+---------------- 1 ----------------
#       | ImportError: no_such_module
#       +---------------- 2 ----------------
#       | ModuleNotFoundError: another_module
#       +------------------------------------
#     +---------------- 3 ----------------
#     | TypeError: int
#     +------------------------------------

В трассировке перечислены все исключения, входящие в группу исключений. Кроме того, вложенная древовидная структура исключений внутри группы указывается как графически, так и путем перечисления количества подисключений в каждой группе.

Класс ExceptionGroup также может вызываться как обычное исключение Python3, т.е. оператором except без звездочки.

>>> try:
...     raise ExceptionGroup("group", [ValueError(654)])
... except ValueError:
...     print("Handling ValueError")

#   + Exception Group Traceback (most recent call last):
#   |   ...
#   | ExceptionGroup: group (1 sub-exception)
#   +-+---------------- 1 ----------------
#     | ValueError: 654
#     +------------------------------------

Даже если группа исключений содержит ValueError, нельзя обработать ее с помощью except ValueError. Для этого необходимо использовать синтаксис exclude* (со звездочкой). У групп исключений есть несколько атрибутов и методов, которых нет у обычных исключений. В частности, можно получить доступ к .exceptions, чтобы получить кортеж всех подисключений в группе. Перепишем последний пример следующим образом:

>>> try:
...     raise ExceptionGroup("group", [ValueError(654)])
... except* ValueError:
...     print("Handling ValueError")
... except* TypeError:
...     print("Handling TypeError")

# Handling ValueError

Каждое предложение except* обрабатывает группу исключений, которая является подгруппой исходной группы исключений и содержит все исключения, соответствующие данному типу ошибки. Рассмотрим немного более сложный пример:

>>> try:
...     raise ExceptionGroup(
...         "group", [TypeError("str"), ValueError(654), TypeError("int")]
...     )
... except* ValueError as eg:
...     print(f"Handling ValueError: {eg.exceptions}")
... except* TypeError as eg:
...     print(f"Handling TypeError: {eg.exceptions}")

# Handling ValueError: (ValueError(654),)
# Handling TypeError: (TypeError('str'), TypeError('int'))

Обратите внимание, что в этом примере срабатывают оба предложения except*. Это отличается от обычных исключений, где одновременно срабатывает не более одного предложения!

В конечном итоге можно лишь частично обработать группу исключений. Например, обработать только ValueError из предыдущего примера:

>>> try:
...     raise ExceptionGroup(
...         "group", [TypeError("str"), ValueError(654), TypeError("int")]
...     )
... except* ValueError as eg:
...     print(f"Handling ValueError: {eg.exceptions}")

# Handling ValueError: (ValueError(654),)
#   + Exception Group Traceback (most recent call last):
#   |   ...
#   | ExceptionGroup: group (2 sub-exceptions)
#   +-+---------------- 1 ----------------
#     | TypeError: str
#     +---------------- 2 ----------------
#     | TypeError: int
#     +------------------------------------

В этом случае обрабатывается ValueError. Но это оставляет две необработанные ошибки в группе исключений. Затем эти ошибки всплывают и создают трассировку. Обратите внимание, что ValueError не является частью трассировки, т.к. она уже обработана.

Можно видеть, что оператор except* со звездочкой ведет себя иначе, чем except без звездочки:

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

Эти изменения делают работу с несколькими одновременными ошибками более удобной.