Выявление контекста исключения

tlndr: как сказать в функции, если она вызвана из блока except (прямо/косвенно). python2.7/CPython.

Я использую python 2.7 и пытаюсь предоставить что-то похожее на py3 __context__ для моего настраиваемого класса исключений:

class MyErr(Exception):
    def __init__(self, *args):
        Exception.__init__(self, *args)
        self.context = sys.exc_info()[1]
    def __str__(self):
        return repr(self.args) + ' from ' + repr(self.context)

Это работает нормально:

try:
   1/0
except:
   raise MyErr('bang!')

#>__main__.MyErr: ('bang!',) from ZeroDivisionError('integer division or modulo by zero',)

Иногда мне нужно MyErr быть поднятым вне блока исключений. Это тоже хорошо:

raise MyErr('just so')

#>__main__.MyErr: ('just so',) from None

Если, однако, перед этим пунктом было обработано исключение, оно неправильно задано как контекст MyErr:

try:
    print xxx
except Exception as e:
    pass

# ...1000 lines of code....
raise MyErr('look out')

#>__main__.MyErr: ('look out',) from NameError("name 'xxx' is not defined",) <-- BAD

Я думаю, причина в том, что sys.exc_info просто возвращает "последнее", а не "текущее" исключение:

Эта функция возвращает кортеж из трех значений, которые предоставляют информацию об исключении, которое в настоящее время обрабатывается. <... > Здесь "обработка исключения" определяется как "выполнение или с выполнением предложения except".

Итак, мой вопрос: как определить, выполняет ли интерпретатор предложение except (и не выполнял его в прошлом). Другими словами: существует ли способ узнать в MyErr.__init__, если в стеке есть except?

Мое приложение не переносимо, любые специальные хаки Cpython приветствуются.

Ответы

Ответ 1

Это проверено с помощью CPython 2.7.3:

$ python myerr.py 
MyErr('bang!',) from ZeroDivisionError('integer division or modulo by zero',)
MyErr('nobang!',)

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

import sys
import opcode

SETUP_EXCEPT = opcode.opmap["SETUP_EXCEPT"]
SETUP_FINALLY = opcode.opmap["SETUP_FINALLY"]
END_FINALLY = opcode.opmap["END_FINALLY"]

def try_blocks(co):
    """Generate code positions for try/except/end-of-block."""
    stack = []
    code = co.co_code
    n = len(code)
    i = 0
    while i < n:
        op = ord(code[i])
        if op in (SETUP_EXCEPT, SETUP_FINALLY):
            stack.append((i, i + ord(code[i+1]) + ord(code[i+2])*256))
        elif op == END_FINALLY:
            yield stack.pop() + (i,)
        i += 3 if op >= opcode.HAVE_ARGUMENT else 1

class MyErr(Exception):
    """Magic exception."""

    def __init__(self, *args):
        callee = sys._getframe(1)
        try:
            in_except = any(i[1] <= callee.f_lasti < i[2] for i in try_blocks(callee.f_code))
        finally:
            callee = None

        Exception.__init__(self, *args)
        self.cause = sys.exc_info()[1] if in_except else None

    def __str__(self):
        return "%r from %r" % (self, self.cause) if self.cause else repr(self)

if __name__ == "__main__":
    try:
        try:
            1/0
        except:
            x = MyErr('bang!')
            raise x
    except Exception as exc:
        print exc

    try:
        raise MyErr('nobang!')
    except Exception as exc:
        print exc
    finally:
        pass

И помните: "Явный лучше, чем неявный", поэтому было бы лучше, если вы спросите меня:

try:
    …
except Exception as exc:
    raise MyErr("msg", cause=exc)

Ответ 2

Следующий подход может работать, хотя он немного длинный.

  • Получить код текущего кадра из import inspect; inspect.currentframe().f_code
  • Осмотрите байт-код (f_code.co_code), возможно, используя dis.dis, чтобы выяснить, выполняется ли кадр в блоке except.
  • В зависимости от того, что вы хотите сделать, вам может потребоваться вернуться к кадру и посмотреть, не вызван ли он из блока except.

Пример:

def infoo():
    raise MyErr("from foo in except")

try:
    nope
except:
    infoo()
  • Если ни один из кадров не находится в блоке except, тогда sys.exc_info() устарел.

Ответ 3

Одним из решений было бы вызвать sys.exc_clear() после обработки исключения:

import sys

class MyErr(Exception):
    def __init__(self, *args):
        Exception.__init__(self, *args)
        self.context = sys.exc_info()[1]
    def __str__(self):
        return repr(self.args) + ' from ' + repr(self.context)

try:
    print xxx
except Exception as e:
    # exception handled
    sys.exc_clear()

raise MyErr('look out')

дает:

Traceback (most recent call last):
  File "test.py", line 18, in <module>
    raise MyErr('look out')`
__main__.MyErr: ('look out',) from None

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

Ответ 4

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

Я нашел этот fblocktype enum, который хранится в fblockinfo struct:

enum fblocktype { LOOP, EXCEPT, FINALLY_TRY, FINALLY_END };

struct fblockinfo {
    enum fblocktype fb_type;
    basicblock *fb_block;
};

Есть комментарий выше fblocktype, который описывает блок кадра:

Блок кадров используется для обработки циклов, try/except и try/finally. Он называл блок кадра, чтобы отличить его от базового блока в компилятор IR.

И затем, когда вы немного подходите, есть описание базового блока:

Каждый базовый блок в блоке компиляции связан через b_list в обратный порядок выделения блока. b_list указывает на следующий блок, чтобы не путать с b_next, который следующий по потоку управления.

Также читайте здесь несколько о Графах потока управления:

График потока управления (часто ссылающийся на его аббревиатуру, CFG) - это ориентированный граф, который моделирует поток программы с использованием базовых блоков которые содержат промежуточное представление (сокращенно "IR", а в этот случай - байт-код Python) внутри блоков. Основные блоки сами являются блоком IR, который имеет единственную точку входа, но возможно, несколько точек выхода. Единственная точка входа - это ключ к базовые блоки; все это связано с прыжками. Точкой входа является цель чего-то, что изменяет поток управления (например, вызов функции или прыжок), в то время как точки выхода являются инструкциями, которые поток программы (например, прыжки и "операторы возврата" ). Что это означает, что базовый блок представляет собой кусок кода, который начинается с точка входа и выполняется до точки выхода или конца блока.

Итак, все это указывает на то, что блок кадра в дизайне Python рассматривается как временный объект. Он не включается напрямую в График потока управления, кроме как часть общего байтового кода базового блока, поэтому кажется, что он не может быть запрошен без анализа байтового кода фреймов.

Далее, я думаю, что причина в вашем примере sys.exc_info показывает исключение из блока try, потому что он хранит последнее исключение из текущего базового блока, блоки кадра здесь не рассматриваются.

sys.exc_info()

Эта функция возвращает кортеж из трех значений, которые предоставляют информацию об исключении, которое в настоящее время обрабатывается. Информация возвращается как конкретный, так и текущий поток и текущий стек кадров. Если текущий стек стека не обрабатывает исключение, информация берется из фрейма вызывающего стека или его вызывающего абонента, и так далее, пока не будет найден стек стека, который обрабатывает исключение. Здесь "обработка исключения" определяется как "выполнение или выполнил предложение except." Для любого кадра стека только информация о последнем обработанном исключении.

Поэтому, когда он говорит о стеке стека, я думаю, что это конкретно означает базовый блок, и все разговоры "обработка исключения" означают, что исключения в блоке кадра, например try/except, for и т.д. пузырь до основного блока выше.