Как повторно вызвать исключение во вложенных блоках try / except?

109

Я знаю, что если я хочу повторно вызвать исключение, я просто использую raiseбез аргументов в соответствующем exceptблоке. Но учитывая вложенное выражение вроде

try:
    something()
except SomeError as e:
    try:
        plan_B()
    except AlsoFailsError:
        raise e  # I'd like to raise the SomeError as if plan_B()
                 # didn't raise the AlsoFailsError

как я могу повторно поднять SomeErrorбез нарушения трассировки стека? raiseодна только в этом случае повторно повысит более недавний AlsoFailsError. Или как мне провести рефакторинг кода, чтобы избежать этой проблемы?

Тобиас Кинцлер
источник
2
Вы пробовали добавить plan_Bдругую функцию, которая возвращается Trueв случае успеха или Falseисключения? Тогда внешний exceptблок мог быть простоif not try_plan_B(): raise
Дрю МакГоуэн
@DrewMcGowen К сожалению, более реалистичный случай заключается в том, что это внутри функции, принимающей произвольные объекты, argи я бы попробовал вызвать, arg.plan_B()который может вызвать AttributeErrorиз-за argнепредоставления плана B
Тобиас Кинцлер
Взгляните на модуль трассировки
Пако,
@Paco Спасибо, я сделаю (хотя ответ уже показывает более простой способ)
Тобиас Кинцлер
@DrewMcGowen Я написал ответ на основе вашего комментария , который выглядит менее питоническим, чем ответ user4815162342 . Но это из-за того, что я хочу иметь возвращаемое значение и позволять plan_Bвызывать исключения
Тобиас Кинцлер

Ответы:

131

Что касается Python 3, трассировка сохраняется в исключении, поэтому простой raise e(в большинстве случаев) будет делать правильные вещи:

try:
    something()
except SomeError as e:
    try:
        plan_B()
    except AlsoFailsError:
        raise e  # or raise e from None - see below

Созданная трассировка будет включать дополнительное уведомление, которое SomeErrorпроизошло во время обработки AlsoFailsError(из-за raise eнахождения внутри except AlsoFailsError). Это вводит в заблуждение, потому что на самом деле произошло обратное: мы столкнулись AlsoFailsErrorи справились с этим, пытаясь оправиться SomeError. Чтобы получить трассировку, которая не включает AlsoFailsError, замените raise eна raise e from None.

В Python 2 вы бы сохранили тип исключения, значение и трассировку в локальных переменных и использовали бы формуraise с тремя аргументами :

try:
    something()
except SomeError:
    t, v, tb = sys.exc_info()
    try:
        plan_B()
    except AlsoFailsError:
        raise t, v, tb
пользователь4815162342
источник
Отлично, вот что я здесь тоже нашел , спасибо! Хотя есть предложение raise self.exc_info[1], None, self.exc_info[2]после self.exc_info = sys.exc_info()- [1]по какой-то причине поставить на первое место
Тобиаса Кинцлера
3
@TobiasKienzler raise t, None, tbпотеряет значение исключения и заставит raiseповторно создать его экземпляр из типа, давая вам менее конкретное (или просто неправильное) значение исключения. Например, если KeyError("some-key")возникло исключение , оно просто повторно вызовет KeyError()и пропустит точный отсутствующий ключ из трассировки.
user4815162342
3
@TobiasKienzler В Python 3 все еще должно быть возможно выразить это как raise v.with_traceback(tb). (Ваш комментарий даже говорит об этом, за исключением того, что он предлагает повторно создать экземпляр значения.)
user4815162342
2
Кроме того, красное предупреждение не хранить sys.exc_info()в локальной переменной имело смысл до Python 2.0 (выпущенного 13 лет назад), но сегодня граничит с нелепостью. Современный Python был бы практически бесполезен без сборщика циклов, поскольку каждая нетривиальная библиотека Python создает циклы без пауз и зависит от их правильной очистки.
user4815162342
1
@ user4815162342 Вы можете убить вложенную ошибку «произошла другая ошибка», написав «поднять e из None».
Matthias
19

Даже если принятое решение правильное, хорошо указать на библиотеку Six, в которой есть решение Python 2 + 3, используя six.reraise.

шесть. ререйз ( exc_type , exc_value , exc_traceback = None)

Повторно вызовите исключение, возможно, с другой трассировкой. [...]

Итак, вы можете написать:

import six


try:
    something()
except SomeError:
    t, v, tb = sys.exc_info()
    try:
        plan_B()
    except AlsoFailsError:
        six.reraise(t, v, tb)
Лоран ЛАПОРТ
источник
1
Хороший момент - говоря о шести, вы также можете использовать, six.raise_fromесли хотите включить информацию, которая plan_B()также потерпела неудачу.
Тобиас Кинцлер
1
@TobiasKienzler: Я думаю, это другое использование: когда six.raise_fromвы создаете новое исключение, которое связано с предыдущим, вы не поднимаете его повторно , поэтому обратная трассировка будет другой.
Laurent LAPORTE
1
Моя точка зрения - если у reraiseвас создается впечатление, что это только something()бросил SomeError, если raise_fromвы также знаете, что это вызвало plan_B()выполнение, но бросание AlsoFailsError. Так что это зависит от варианта использования. Думаю raise_from, упростит отладку
Тобиас Кинцлер
9

Согласно предложению Дрю МакГоуэна , но заботясь об общем случае (где присутствует возвращаемое значение s), вот альтернатива ответу user4815162342 :

try:
    s = something()
except SomeError as e:
    def wrapped_plan_B():
        try:
            return False, plan_B()
        except:
            return True, None
    failed, s = wrapped_plan_B()
    if failed:
        raise
Тобиас Кинцлер
источник
1
В этом подходе хорошо то, что он работает без изменений в Python 2 и 3.
user4815162342
2
@ user4815162342 Хороший момент :) Хотя пока что в Python3 я бы подумал raise from, поэтому трассировка стека также позволила бы мне потерпеть неудачу в плане B. Что, кстати, можно эмулировать в Python 2 .
Тобиас Кинцлер,
5

Python 3.5+ в любом случае прикрепляет информацию трассировки к ошибке, поэтому больше нет необходимости сохранять ее отдельно.

>>> def f():
...   try:
...     raise SyntaxError
...   except Exception as e:
...     err = e
...     try:
...       raise AttributeError
...     except Exception as e1:
...       raise err from None
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in f
  File "<stdin>", line 3, in f
SyntaxError: None
>>> 
Маттиас Урликс
источник
2
Вопрос в другом исключении, происходящем во время except. Но вы правы, когда я заменяю err = e, скажем,, raise AttributeErrorвы сначала получаете SyntaxErrorтрассировку стека, а затем - During handling of the above exception, another exception occurred:и AttributeErrorтрассировку стека. Полезно знать, хотя, к сожалению, нельзя полагаться на установку версии 3.5+. PS: ff verstehen nicht-Deutsche vermutlich nicht;)
Тобиас Кинцлер
Хорошо, поэтому я изменил пример, чтобы вызвать другое исключение, которое (как и заданный исходный вопрос) игнорируется, когда я повторно вызываю первое.
Matthias
3
@TobiasKienzler 3.5+ (на который я его изменил), похоже, является всемирно признанным форматом. Был денкст ду? ;)
linusg 05
@linusg Согласен :)
Тобиас Кинцлер