Предыдущая ошибка маскируется текущим контекстом исключения

Ниже приведен пример, который я нашел на веб-сайте для Дуга Хеллмана в файле с именем "masking_exceptions_catch.py". Я не могу найти ссылку на данный момент. Исключение, созданное в throws(), отбрасывается, пока сообщается о том, что вызвано очисткой().

В своей статье Дуг замечает, что обработка не интуитивно понятна. В середине ожидания ожидая, что это ошибка или ограничение в версии Python в то время, когда она была написана (около 2009 года), я запустил ее в текущей производственной версии Python для Mac (2.7.6). Он по-прежнему сообщает об исключении из cleanup(). Я нахожу это несколько удивительным и хотел бы увидеть описание того, как это на самом деле правильное или желательное поведение.

#!/usr/bin/env python

import sys
import traceback

def throws():
    raise RuntimeError('error from throws')

def nested():
    try:
        throws()
    except:
        try:
            cleanup()
        except:
            pass # ignore errors in cleanup
        raise # we want to re-raise the original error

def cleanup():
    raise RuntimeError('error from cleanup')

def main():
    try:
        nested()
        return 0
    except Exception, err:
        traceback.print_exc()
        return 1

if __name__ == '__main__':
    sys.exit(main())

Выход программы:

$ python masking_exceptions_catch.py
Traceback (most recent call last):
  File "masking_exceptions_catch.py", line 24, in main
    nested()
  File "masking_exceptions_catch.py", line 14, in nested
    cleanup()
  File "masking_exceptions_catch.py", line 20, in cleanup
    raise RuntimeError('error from cleanup')
RuntimeError: error from cleanup

Ответ 1

Вернитесь назад, чтобы ответить. Начну с того, что не отвечу на ваш вопрос.: -)

Это действительно работает?

def f():
    try:
        raise Exception('bananas!')
    except:
        pass
    raise

Итак, что делает вышеизложенное? Cue Jeopardy music.


Хорошо, тогда карандаши вниз.

# python 3.3
      4     except:
      5         pass
----> 6     raise
      7 

RuntimeError: No active exception to reraise

# python 2.7
      1 def f():
      2     try:
----> 3         raise Exception('bananas!')
      4     except:
      5         pass

Exception: bananas!

Хорошо, это было плодотворно. Для удовольствия попробуйте назвать исключение.

def f():
    try:
        raise Exception('bananas!')
    except Exception as e:
        pass
    raise e

Что теперь?

# python 3.3
      4     except Exception as e:
      5         pass
----> 6     raise e
      7 

UnboundLocalError: local variable 'e' referenced before assignment

# python 2.7
      4     except Exception as e:
      5         pass
----> 6     raise e
      7 

Exception: bananas!

Семантика исключений сильно изменилась между python 2 и 3. Но если поведение python 2 для вас вообще удивительно, подумайте: в основном это соответствует тому, что питон делает везде.

try:
    1/0
except Exception as e: 
    x=4
#can I access `x` here after the exception block?  How about `e`?

try и except не являются областями. На самом деле мало что происходит на питоне; у нас есть "правило LEGB", чтобы запомнить четыре пространства имен: "Локальные", "Закрывающиеся", "Глобальные", "Встроенные". Другие блоки просто не являются областями; Я могу с радостью объявить x в цикле for и ожидать, что все еще сможет ссылаться на него после этого цикла.

Итак, неудобно. Должны ли исключения быть закрытыми для закрытого лексического блока? Python 2 говорит нет, python 3 говорит yes. Но я упрощаю вещи здесь; Голый raise - это то, о чем вы сначала спросили, и проблемы тесно связаны, но на самом деле не совпадают. Python 3 мог бы утверждать, что именованные исключения привязаны к их блоку, не обращаясь к голой теме raise.

Что горит raise do‽

Общее использование - использовать bare raise как средство сохранения трассировки стека. Поймать, выполнить регистрацию/очистку, ререйз. Прохладный, мой код очистки не отображается в traceback, работает 99,9% времени. Но все может идти на юг, когда мы пытаемся обрабатывать вложенные исключения в обработчике исключений. Иногда. (см. примеры внизу, когда это/не проблема)

Интуитивно, no-argument raise будет правильно обрабатывать вложенные обработчики исключений и вычислять правильное "текущее" исключение для ререйза. Однако это не совсем реальность. Оказывается, что - вхождение в подробности реализации здесь - информация об исключении сохраняется как член текущего объекта фрейма. И в python 2 просто нет сантехники для обработки push-popping-обработчиков исключений в стеке в пределах одного кадра; просто просто поле, содержащее последнее исключение, независимо от любой обработки, которую мы могли бы сделать с ней. То, что горит raise.

6,9. Операция raise

raise_stmt ::= "raise" [expression ["," expression ["," expression]]]

Если выражения не присутствуют, рейз повторно вызывает последнее исключение, был активен в текущей области.

Итак, да, это проблема глубоко внутри python 2, связанная с тем, как хранится информация о трассировке - в традиции Highlander может быть только один объект трассировки, сохраненный в данном стеке стека. Как следствие, голый raise ререйзит, который, по мнению текущего кадра, является "последним" исключением, что не обязательно является тем, которое, по нашему мнению, является человеческим мозгом, является специфическим для лексически-вложенного блока исключений, на котором мы находимся время. Ба, области!

Итак, исправлено в python 3?

Да. Как? Новая инструкция байткода (две, фактически, есть еще одна неявная в начале исключения обработчиков), но на самом деле кто заботится - все это "просто работает" интуитивно, Вместо получения RuntimeError: error from cleanup ваш примерный код поднимает RuntimeError: error from throws, как ожидалось.

Я не могу дать вам официальную причину, почему это не было включено в python 2. Проблема была известна с PEP 344, которая упоминает Раймонда Хеттингера, поднимающего проблему в 2003 году. Если бы я должен был догадаться, исправление этого является нарушающим изменением (среди прочего, это влияет на семантику sys.exc_info), и это часто является достаточно хорошей причиной, чтобы не делать этого в незначительном выпуск.

Параметры, если вы находитесь на python 2:

1) Назовите исключение, которое вы планируете ререйзировать, и просто обработайте строку или две, которые будут добавлены в конец вашей трассировки стека. Ваш пример nested будет выглядеть следующим образом:

def nested():
    try:
        throws()
    except BaseException as e:
        try:
            cleanup()
        except:
            pass 
        raise e

И связанная трассировка:

Traceback (most recent call last):
  File "example", line 24, in main
    nested()
  File "example", line 17, in nested
    raise e
RuntimeError: error from throws

Итак, трассировка изменена, но она работает.

1.5) Используйте версию с тремя аргументами raise. Многие люди не знают об этом, и это законный (неуклюжий) способ сохранить трассировку стека.

def nested():
    try:
        throws()
    except:
        e = sys.exc_info()
        try:
            cleanup()
        except:
            pass 
        raise e[0],e[1],e[2]

sys.exc_info дает нам 3-кортеж, содержащий (тип, значение, трассировку), что и делает 3-аргументная версия raise. Обратите внимание, что этот синтаксис 3-arg работает только в python 2.

2) Рефакторинг вашего кода очистки таким образом, чтобы он не мог вызывать необработанное исключение. Помните, что все о областях - переместите try/except из nested и в свою собственную функцию.

def nested():
    try:
        throws()
    except:
        cleanup()
        raise

def cleanup():
    try:
        cleanup_code_that_totally_could_raise_an_exception()
    except:
        pass

def cleanup_code_that_totally_could_raise_an_exception():
    raise RuntimeError('error from cleanup')

Теперь вам не нужно беспокоиться; поскольку исключение никогда не попадало в область nested, оно не будет мешать исключению, которое вы планируете делать повторно.

3) Используйте голый raise, как вы делали, прежде чем читать все это и жить с ним; код очистки обычно не вызывает исключений, правильно?: -)