Это сложный вопрос для формулировки, но вот урезанная версия ситуации. Я использую код библиотеки, который принимает обратный вызов. Он имеет собственную обработку ошибок и выдает ошибку, если что-то пойдет не так во время выполнения обратного вызова.
class LibraryException(Exception):
pass
def library_function(callback, string):
try:
# (does other stuff here)
callback(string)
except:
raise LibraryException('The library code hit a problem.')
Я использую этот код внутри цикла ввода. Я знаю о потенциальных ошибках, которые могут возникнуть в моей функции обратного вызова в зависимости от значений на входе. Если это произойдет, я хотел бы повторить запрос после получения полезного отзыва из сообщения об ошибке. Я представляю, что это выглядит примерно так:
class MyException(Exception):
pass
def my_callback(string):
raise MyException("Here's some specific info about my code hitting a problem.")
while True:
something = input('Enter something: ')
try:
library_function(my_callback, something)
except MyException as e:
print(e)
continue
Конечно, это не сработает, потому что MyException
будет пойман внутри library_function
, что вызовет собственное (гораздо менее информативное) Exception
и остановит программу.
Очевидно, что нужно проверить мой ввод перед вызовом library_function
, но это круговая проблема, потому что синтаксический анализ — это то, для чего я использую код библиотеки в первую очередь. (Для любопытных, это Ларк, но я не думаю, что мой вопрос достаточно специфичен для Ларк, чтобы загромождать его всеми конкретными деталями.)
Одной из альтернатив было бы изменить мой код, чтобы он ловил любую ошибку (или, по крайней мере, тип ошибки, которую генерирует библиотека), и напрямую печатать внутреннее сообщение об ошибке:
def my_callback(string):
error_str = "Here's some specific info about my code hitting a problem."
print(error_str)
raise MyException(error_str)
while True:
something = input('Enter something: ')
try:
library_function(my_callback, something)
except LibraryException:
continue
Но я вижу в этом две проблемы. Во-первых, я забрасываю широкую сеть, потенциально выявляя и игнорируя ошибки, отличные от тех, к которым я стремлюсь. Кроме того, просто кажется... неэлегантным и однообразным печатать сообщение об ошибке, а затем выбрасывать само исключение в пустоту. Кроме того, цикл событий командной строки предназначен только для тестирования; в конечном итоге я планирую встроить это в приложение с графическим интерфейсом, и без вывода на печать я все равно захочу получить доступ и отобразить информацию о том, что пошло не так.
Какой самый чистый и самый Pythonic способ добиться чего-то подобного?
@Chase 1) Что, если библиотека выдает исключение того же типа и по другим причинам? Или что, если есть некоторые ситуации, в которых ошибки моего собственного кода могут привести к тому, что библиотека выдаст немного другое исключение? Я не чувствую, что это полностью безопасно или рассчитано на будущее. 2) Мне все равно придется напрямую печатать свою собственную информацию об ошибке ... не так уж и сложно, но все же кажется неудобным.
Возможно, в вашем последнем примере изменение продолжит поднимать MyExeption. Или проверяйте ввод пользователя без исключений. Например, если в пользовательском вводе есть буквы, скажите пользователю не вводить буквы.
@RocketNikita Не было бы особого смысла поднимать MyException
во внешнем try
без соответствующей информации об ошибке из того, что на самом деле происходит на нижнем уровне. Вход находится в DSL, который я анализирую, поэтому проверить его не так просто, как просто убедиться, что все его символы относятся к определенному типу и т. д.
Я думаю, вам не повезло, если библиотека «маскирует» внутренние исключения вместо того, чтобы связывать их в цепочку, я не думаю, что у вас есть способ получить внутреннее исключение. как насчет хранения информации, которую вы использовали бы для своего исключения, в другом месте (env var/file)? затем прочитайте информацию из него, когда поймаете общее исключение библиотеки...
@yurib не обязательно, python сам отслеживает историю исключений. Целое «Во время обработки исключения A произошло исключение B». Это, вероятно, то, что ищет ОП
@yurib: см. PEP 3134 и атрибуты __cause__
и __context__
.
Я предполагаю, что traceback.walk_tb может помочь «развернуть» трассировку под рукой, возможно, столкнувшись с любыми предыдущими исключениями, которые вызвали текущее исключение по пути. Хотя я бы предпочел проверить это, прежде чем делать заявление. Для конкретного примера OP это работает, но независимо от того, надежно оно или нет, требуется некоторое тестирование.
Также следует отметить, что traceback.TracebackException(*sys.exc_info()).__context__
в блоке except
также даст вам прямое сообщение из предыдущего исключения. traceback.format_exc
сам по себе даст вам невероятно удобную строку со всеми исключениями в цепочке. Но чтобы извлечь саму цепочку исключений, вам нужно будет проанализировать ее самостоятельно. Хотя это, вероятно, самый простой способ, возможно, он не самый идеальный.
Кажется, есть много способов добиться желаемого. Хотя, какой из них более надежный - я не могу найти подсказку. Я постараюсь объяснить все методы, которые показались мне очевидными. Возможно, вы найдете один из них полезным.
Я буду использовать предоставленный вами пример кода, чтобы продемонстрировать эти методы, вот свежая информация о том, как это выглядит:
class MyException(Exception):
pass
def my_callback(string):
raise MyException("Here's some specific info about my code hitting a problem.")
def library_function(callback, string):
try:
# (does other stuff here)
callback(string)
except:
raise Exception('The library code hit a problem.')
import traceback
try:
library_function(my_callback, 'boo!')
except:
# NOTE: Remember to keep the `chain` parameter of `format_exc` set to `True` (default)
tb_info = traceback.format_exc()
Это не требует особых знаний об исключениях и самих трассировках стека, а также не требует от вас передачи какого-либо специального фрейма/трассировки/исключения в библиотечную функцию. Но посмотрите, что это возвращает (например, значение tb_info
) -
'''
Traceback (most recent call last):
File "path/to/test.py", line 14, in library_function
callback(string)
File "path/to/test.py", line 9, in my_callback
raise MyException("Here's some specific info about my code hitting a problem.")
MyException: Here's some specific info about my code hitting a problem.
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "path/to/test.py", line 19, in <module>
library_function(my_callback, 'boo!')
File "path/to/test.py", line 16, in library_function
raise Exception('The library code hit a problem.')
Exception: The library code hit a problem.
'''
Это строка, то же самое, что вы увидите, если просто позволите исключению произойти без перехвата. Обратите внимание на цепочку исключений здесь, исключение вверху — это исключение, которое произошло до исключения внизу. Вы можете разобрать все имена исключений-
import re
exception_list = re.findall(r'^(\w+): (\w+)', tb_info, flags=re.M)
При этом вы получите [('MyException', "Here's some specific info about my code hitting a problem"), ('Exception', 'The library code hit a problem')]
в exception_list
Хотя это самый простой выход, он не очень контекстно-зависим. Я имею в виду, что все, что вы получаете, это имена классов в строковой форме. В любом случае, если это то, что соответствует вашим потребностям - я не вижу в этом особой проблемы.
__context__
/__cause__
Сам Python отслеживает историю трассировки исключений, текущее исключение, исключение, вызвавшее это исключение, и так далее. Вы можете прочитать о сложных деталях этой концепции в PEP 3134
Независимо от того, пройдете ли вы весь PEP или нет, я призываю вас хотя бы ознакомиться с неявно связанными исключениями и явными связанными исключениями. Возможно этот ТАК тред будет полезен для этого.
Небольшое напоминание: raise ... from
предназначен для явной цепочки исключений. Метод, который вы показываете в своем примере, представляет собой неявную цепочку
Теперь вам нужно сделать заметку: TracebackException#__cause__ предназначен для исключений с явной цепочкой, а TracebackException#__context__ — для исключений с неявной цепочкой. Поскольку в вашем примере используется неявная цепочка, вы можете просто следовать __context__
назад, и вы достигнете MyException
. На самом деле, поскольку это всего лишь один уровень вложенности, вы достигнете его мгновенно!
import sys
import traceback
try:
library_function(my_callback, 'boo!')
except:
previous_exc = traceback.TracebackException(*sys.exc_info()).__context__
Сначала создается TracebackException
из sys.exc_info. sys.exc_info
возвращает кортеж (exc_type, exc_value, exc_traceback)
для имеющегося исключения (если есть). Обратите внимание, что эти 3 значения в указанном порядке — это именно то, что вам нужно для построения TracebackException
, поэтому вы можете просто деструктурировать его с помощью *
и передать конструктору класса.
Это возвращает объект TracebackException
о текущем исключении. Исключение, из которого оно неявно связано, находится в __context__
, исключение, из которого оно явно связано, находится в __cause__
.
Обратите внимание, что и __cause__
, и __context__
вернут либо объект TracebackException
, либо None
(если вы находитесь в конце цепочки). Это означает, что вы можете снова вызвать __cause__
/__context__
для возвращаемого значения и в основном продолжать работать, пока не достигнете конца цепочки.
Печать объекта TracebackException
просто печатает сообщение об исключении, если вы хотите получить сам класс (фактический класс, а не строку), вы можете сделать .exc_type
print(previous_exc)
# prints "Here's some specific info about my code hitting a problem."
print(previous_exc.exc_type)
# prints <class '__main__.MyException'>
Вот пример рекурсии по .__context__
и печати типов всех исключений в неявной цепочке. (Вы можете сделать то же самое для .__cause__
)
def classes_from_excs(exc: traceback.TracebackException):
print(exc.exc_type)
if not exc.__context__:
# chain exhausted
return
classes_from_excs(exc.__context__)
Давайте использовать его!
try:
library_function(my_callback, 'boo!')
except:
classes_from_excs(traceback.TracebackException(*sys.exc_info()))
Это напечатает-
<class 'Exception'>
<class '__main__.MyException'>
Еще раз, смысл этого в том, чтобы быть в курсе контекста. В идеале печать — это не то, что вам нужно делать в практической среде, у вас есть сами объекты класса со всей информацией!
ПРИМЕЧАНИЕ. Для исключений с неявной цепочкой, если исключение явно подавлено, попытка восстановить цепочку будет неудачной. В любом случае вы можете попробовать __supressed_context__.
Это, вероятно, самое близкое к низкоуровневой обработке исключений. Если вы хотите захватить целые кадры информации, а не только классы исключений, сообщения и тому подобное, вы можете найти walk_tb
полезным... и немного болезненным.
import traceback
try:
library_function(my_callback, 'foo')
except:
tb_gen = traceback.walk_tb(sys.exc_info()[2])
Здесь... слишком много всего для обсуждения. .walk_tb
принимает объект трассировки, вы, возможно, помните из предыдущего метода, что 2-й индекс возвращаемого кортежа из sys.exec_info
— это именно то, что нужно. Затем он возвращает генератор кортежей объекта фрейма и int (Iterator[Tuple[FrameType, int]]
).
Эти объекты кадра содержат всевозможную сложную информацию. Хотя, найдете ли вы именно то, что ищете, это уже другая история. Они могут быть сложными, но они не являются исчерпывающими, если вы не играете с большим количеством проверок кадра. Несмотря на это, это то, что представляют собой объекты кадра.
Что вы будете делать с кадрами, решать вам. Их можно передать многим функциям. Вы можете передать весь генератор в StackSummary.extract, чтобы получить объекты framesummary, вы можете перебирать каждый кадр, чтобы посмотреть на [0].f_locals
([0]
на Tuple[FrameType, int]
возвращает фактический объект кадра) и так далее.
for tb in tb_gen:
print(tb[0].f_locals)
Это даст вам список местных жителей для каждого кадра. В первом tb
из tb_gen
вы увидите MyException
как часть местных жителей... среди множества других вещей.
У меня ползучее чувство, что я проглядел некоторые методы, скорее всего, с inspect. Но я надеюсь, что вышеперечисленные методы будут достаточно хороши, чтобы никому не пришлось проходить через беспорядок, который есть inspect
:P
Ух ты! Какая исключительная рецензия, если вы простите за каламбур. Я написал функцию, основанную на втором подходе, которая хорошо работает на данный момент и кажется легко расширяемой для моих будущих нужд; Я думаю, что опубликую это как еще один ответ, для полноты картины. Спасибо!
отличный ответ, признаю ;)
Ответ Чейза выше феноменален. Для полноты картины вот как я реализовал их второй подход в этой ситуации. Во-первых, я сделал функцию, которая может искать в стеке указанный тип ошибки. Несмотря на то, что цепочка в моем примере неявная, она должна следовать неявной и/или явной цепочке:
import sys
import traceback
def find_exception_in_trace(exc_type):
"""Return latest exception of exc_type, or None if not present"""
tb = traceback.TracebackException(*sys.exc_info())
prev_exc = tb.__context__ or tb.__cause__
while prev_exc:
if prev_exc.exc_type == exc_type:
return prev_exc
prev_exc = prev_exc.__context__ or prev_exc.__cause__
return None
При этом все просто:
while True:
something = input('Enter something: ')
try:
library_function(my_callback, something)
except LibraryException as exc:
if (my_exc := find_exception_in_trace(MyException)):
print(my_exc)
continue
raise exc
Таким образом, я могу получить доступ к своему внутреннему исключению (и распечатать его на данный момент, хотя в конечном итоге я могу делать с ним другие вещи) и продолжить. Но если моего исключения там не было, я просто повторно поднимаю то, что подняла библиотека. Идеальный!
почему бы не поймать конкретное исключение, которое вызывает библиотека?