Выявление контекста исключения
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
и т.д. пузырь до основного блока выше.