Python «поднимает» без аргументов: какое последнее исключение было активным в текущей области?

В документации Python сказано:

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

(Python 3: https://docs.python.org/3/reference/simple_stmts.html#raise; Python 2.7: https://docs.python.org/2.7/reference/simple_stmts.html#raise.)

Однако понятие «последний активный», похоже, изменилось. Посмотрите на следующий пример кода:

#
from __future__ import print_function
import sys
print('Python version =', sys.version)

try:
    raise Exception('EXPECTED')
except:
    try:
        raise Exception('UNEXPECTED')
    except:
        pass
    raise # re-raises UNEXPECTED for Python 2, and re-raises EXPECTED for Python 3

что приводит к тому, чего я не ожидал от Python 2:

Python version = 2.7.15 (v2.7.15:ca079a3ea3, Apr 30 2018, 16:30:26) [MSC v.1500 64 bit (AMD64)]
Traceback (most recent call last):
  File "./x", line 10, in <module>
    raise Exception('UNEXPECTED')
Exception: UNEXPECTED

но имеет ожидаемый (мной) результат с Python 3:

Python version = 3.6.8 (default, Feb 14 2019, 22:09:48)
[GCC 7.4.0]
Traceback (most recent call last):
  File "./x", line 7, in <module>
    raise Exception('EXPECTED')
Exception: EXPECTED

а также

Python version = 3.7.2 (tags/v3.7.2:9a3ffc0492, Dec 23 2018, 23:09:28) [MSC v.1916 64 bit (AMD64)]
Traceback (most recent call last):
  File "./x", line 7, in <module>
    raise Exception('EXPECTED')
Exception: EXPECTED

Так что же значит "последний... активный"? Есть ли документация по этому критическому изменению? Или это ошибка Python 2?

И что еще более важно: как лучше всего заставить это работать в Python 2? (Желательно, чтобы код продолжал работать в Python 3.)


Обратите внимание, если изменить код на

#
from __future__ import print_function
import sys
print('Python version =', sys.version)

def f():
    try:
        raise Exception('UNEXPECTED')
    except:
        pass

try:
    raise Exception('EXPECTED')
except:
    f()
    raise # always raises EXPECTED

тогда все начинает работать и для Python 2:

Python version = 2.7.15 (v2.7.15:ca079a3ea3, Apr 30 2018, 16:30:26) [MSC v.1500 64 bit (AMD64)]
Traceback (most recent call last):
  File "./x", line 13, in <module>
    raise Exception('EXPECTED')
Exception: EXPECTED

Думаю перейти на него...


person MarnixKlooster ReinstateMonica    schedule 03.06.2019    source источник
comment
Связано: cosmicpercolator.com/2016/01 /13/   -  person wim    schedule 04.06.2019


Ответы (2)


Поведение Python 2 — это не столько ошибка, сколько недостаток дизайна. В Python 3.0 эта проблема была устранена путем добавления функций цепочки исключений. Ближайшую документацию по этому изменению можно найти в PEP 3134 -- Exception Цепочка и встроенная обратная трассировка мотивация:

Во время обработки одного исключения (исключения A) может возникнуть другое исключение (исключение B). В современном Python (версия 2.4), если это происходит, исключение B распространяется наружу, а исключение A теряется.

Это именно то, что вы видите в 2.7: ОЖИДАЕМОЕ (A) было потеряно, потому что появилось НЕОЖИДАННОЕ (B) и перезаписало его. Благодаря новым функциям цепочки исключений в Python 3 полный контекст обеих ошибок может быть сохранен с помощью атрибутов __cause__ и __context__ в экземплярах исключений.

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

try:
    raise Exception('EXPECTED')
except Exception as err_expected:
    try:
        raise Exception('UNEXPECTED')
    except Exception as err_unexpected:
        pass
    raise err_expected

Если вы хотите подавить функцию цепочки исключений кросс-совместимым способом, вы можете сделать это, установив err_expected.__cause__ = None перед повторным повышением.

person wim    schedule 04.06.2019

raise использует ту же информацию, что и sys.exc_info, которая документирует оба поведение. Так как поведение каждого кадра, которое использует ваш обходной путь, задокументировано, это путь.

PEP 3110 внес несколько изменений в оператор except. Я полагаю, что он включал это, но единственное, что явно упоминается, это то, что исключение, сохраненное as, отбрасывается при выходе из except.

person Davis Herring    schedule 04.06.2019
comment
Этот PEP 3110 в основном не связан с вопросом OP. Отказ от исключения при выходе из блока исключений связан с обходом цикла ссылок exception -> traceback -> stack frame -> exception, вызванного новым атрибутом __traceback__. - person wim; 04.06.2019
comment
@wim: Ну, поведение, похоже, было изменено r62847, но я не нашел, какой (другой?) PEP это может быть. - person Davis Herring; 04.06.2019