Показаны сообщения с ярлыком raise from. Показать все сообщения
Показаны сообщения с ярлыком raise from. Показать все сообщения

среда, 14 марта 2012 г.

PEP 409: подавление контекста исключений

Суть
Одна из открытых проблем со времён PEP 3134 — подавление контекста, так как на тот момент решения не было найдено. Данный PEP исправляет это упущение.
Причина
Есть два способа создать исключение:

  1. Python сам делает это (ошибки кода, отсутствующие ресурсы, завершение циклов и т.д.)
  2. ручками (с помощью выражения raise)
В процессе написания библиотек или пользовательских классов часто необходимо возбуждать исключения; более того, часто полезно, или даже нужно, заменить одно исключение на другое. Вот пример из модуля dbf автора:
try:
    value = int(value)
except Exception:
    raise DbfError(...)
По любому начальное исключение (ValueError, TypeError или что-то в этом роде) уже не релевантно. С этого момента нам нужно только DbfError, однако, если это исключение будет выведено, то мы увидим информацию об обоих исключениях.
Альтернативы
Были предложены следующие возможности решения:

  • raise as NewException()
    Используется ключевое слово as. Может ввести в заблуждение, так как на самом деле мы не перевозбуждаем начальное исключение
  • raise NewException() from None
    В русле текущего синтаксиса объявления предыдущего исключения
  • exc=NewException(); exc.__context__=None; raise exc
    Более многословный способ предыдущего варианта
  • raise NewException.no_context(...)
    Создаем метод класса для подавления контекста
Предложение
Я предлагаю использовать второй вариант:
raise NewException from None
В данном случае мы имеем преимущество в том, что используем существующий шаблон для установки причины возбуждения исключения:
raise KeyError() from NameError()
но поскольку причиной будет None, предыдущий контекст не будет отображаться стандартной процедурой отображения исключения.
Обсуждение реализации
На данный момент None является  значением по умолчанию для __context__ и __cause__. Для обеспечения поддержки raise ... from None (что должно установить значение __cause__ в None) нам нужно другое значение по умолчанию для __cause__. Было предложено несколько идей, как это реализовать на уроне языка:

  • Переписать информацию в имеющемся исключении (пошагово и оставить значение None для  __cause__)
    Отклонено, так как это может серьезно помешать отладке из-за плохого сообщения об ошибке
  • Использовать одно из булевых значений в  __cause__: False может быть значением по умолчанию и при использовании from ... будет заменено либо на соответствующее исключение, либо на None
    Отклонено, так как поощряет использование двух разных типов объектов для  __cause__, причем для одного из этих типов (булева) не возможно использовать полный спектр значений (True не будет использовано)
  • Создать специальный класс исключений __NoException__
    Отклонено, так как возможно будет вводить в заблуждение или будет по ошибке возбуждено пользователями и не является уникальным значением, как None, False и True.
  • Использовать Ellipsis (многоточие) для значения по умолчанию
    Принято.
Многоточие в английском используются там, где опущены какие-либо слова. Так что в нашем случае это будет означать: "__cause__ опущено, так что смотрите в __context__ для получения более подробной информации".
Ellipsis  так же не является исключением, так что не может быть возбуждено.
Есть только Ellipsis, так что не будет неиспользованных значений
Информация об ошибке не выбрасывается, то что пользовательский код может провести трассировку цепи исключений при том, что по умолчанию это не происходит.
Детали языка
Для поддержки raise Exception from None, __context__ останется каким и был, а  __cause__  получит значение по умолчанию Ellipsis и будет заменено на None при использовании выражения  Exception from None.
форма __context__ __cause__
raise None Ellipsis
повторное raise предыдущее исключение Ellipsis
повторное raise from None | ChainedException предыдущее исключение None | цепь исключений
Процедура вывода исключения по умолчанию будет работать так:

  • если  __cause__ равно Ellipsis, __context__ (если есть) будет выведен
  • если  __cause__  равно None, __context__ не будет выведен
  • если  __cause__  содержит любое другое значение, будет отображено  __cause__  
В двух последних случаях следование по цепи исключений будет прекращено.
Поскольку значение по умолчанию для __cause__ теперь Ellipsis, a raise Exception form Cause лишь более короткая форма для:
_exc = NewException()
_exc.__cause__ = Cause()
raise _exc
Ellipsis, как и None, теперь может быть "причиной":
raise Exception from Ellipsis

Выражение raise


raise_stmt ::= "raise" [expression ["from" expression]]
В случае отсутствия expression, повторно возбуждается последнее исключение, которое было активно в данной области. Если такого исключения нет, то возбуждается исключение RuntimeError, чтобы сообщить о данной ошибке.
В противном случае raise выполняет первый expression и получает объект исключения. Он должен являться либо подклассом либо экземпляром BaseException. Если первый expression является именем класса, то создается объект путём вызова класса без передачи аргументов.
Типом данного исключения является класс экземпляра исключения, а значением — сам экземпляр.
Объект трассировки обычно создается автоматически при возбуждении исключения и присоединяется к атрибуту исключения __traceback__, доступному для записи. Вы можете создать исключение и сами установить свою собственную трассировку используя  метод исключения with_traceback() (который возвращает тот же самый экземпляр исключения с установленным аргументом трассировки), как например:
raise Exception("foo occurred").with_traceback(tracebackobj)
Оператор from используется для создания цепочки исключений: если он присутствует, то второй expression должен быть другим классом или экземпляром исключения, которое будет присоединено к возбуждаемому исключению в качестве атрибута  __cause__ (доступного для записи). Если возбуждаемое исключение не перехватывается, то оба исключения будут напечатаны:
>>> try:
...     print(1 / 0)
... except Exception as exc:
...     raise RuntimeError("Something bad happened") from exc...

Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: int division or modulo by zero

The above exception was the direct cause of the following exception:
/*Исключение выше было прямой причиной следующего исключения*/
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
RuntimeError: Something bad happened
Похожий механизм работает если исключение возникло в процессе перехвата исключения: первое исключение будет присоединено к атрибуту __context__ нового исключения:
>>> try:
...     print(1 / 0)
... except:
...     raise RuntimeError("Something bad happened")
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: int division or modulo by zero

During handling of the above exception, another exception occurred:
/*В процессе перехвата вышеуказанного исключения возникло новое исключение:*/

Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
RuntimeError: Something bad happened
Дополнительную информацию про исключения можно найти в разделе Исключения, а про перехват исключений в разделе выражение try.