Иногда, я натыкаюсь на конструкцию вида: try: raise Exception except Exception as e: raise ValueError from e Или даже такую: try: raise Exception except Exception: raise ValueError from None Что это, и для чего используется?
Ответ Конструкция raise from в языке Python используется для того, чтобы сформировать дополнительный контекст (цепочку возникновения) исключения. Синтаксис этой конструкции следующий: raise from
Как это работает: У каждого исключения есть магические атрибуты: __context__ и __cause__, которые содержат в себе контекст возникновения исключения, по умолчанию они всегда инициализируются значением None, это можно наглядно увидеть: try: raise Exception except Exception as e: print(e.__context__) # None print(e.__cause__) # None Но в ряде случаев данные из этих атрибутов будут использованы для вывода дополнительного контекста ошибки.
Расскажу об этом подробнее: Если при обработке (except) одного исключения вызывается другое исключение, то используется атрибут __context__, он будет содержать ссылку на то исключение, которое возникло первым. Убедимся в этом: try: try: raise ValueError except ValueError: raise TypeError except TypeError as e: print(repr(e.__context__)) # ValueError() В случае же, когда исключение вызывается при помощи конструкции raise from используется атрибут __cause__, значением которого становится ссылка на то исключение, которое находится после оператора from (причина исключения): Это тоже можно увидеть: try: try: raise ValueError except ValueError: raise TypeError from IndexError except TypeError as e: print(repr(e.__cause__)) # IndexError() Так как эти атрибуты магические, то вся работа происходит с ними "под капотом", и в действительности запись: raise Exception from ValueError Полностью аналогична следующей последовательности действий: e = Exception() e.__cause__ = ValueError() raise e
Весь дополнительный контекст исключения, который содержат в себе эти атрибуты, конечный пользователь увидит в специфичных ошибках, на которые мы сейчас посмотрим: Случай 1: Вызов одного исключения, во время обработки другого исключения (контекст, который содержит в себе атрибут __context__): При запуске кода: try: raise ValueError except ValueError: raise TypeError Возникает следующая ошибка: Traceback (most recent call last): File "test.py", line 3, in raise ValueError ValueError During handling of the above exception, another exception occurred: Traceback (most recent call last): File "test.py", line 5, in raise TypeError TypeError В тексте ошибки можно заметить следующую строку: During handling of the above exception, another exception occurred: Таким образом, отображается весь необходимый контекст: изначально обрабатывалось исключение ValueError и во время его обработки возникло другое исключение TypeError.
Случай 2: При использовании конструкции raise from (контекст, который содержит в себе атрибут __cause__): Запуск кода: try: raise ValueError except ValueError as e: raise TypeError from e Вызывает следующую ошибку: Traceback (most recent call last): File "C:\Users\Pavel\Desktop andom-from-sv\test.py", line 2, in raise ValueError ValueError The above exception was the direct cause of the following exception: Traceback (most recent call last): File "C:\Users\Pavel\Desktop andom-from-sv\test.py", line 4, in raise TypeError from e TypeError В этой ситуации текст ошибки уже другой, и мы можем наблюдать пояснение о причине исключения: The above exception was the direct cause of the following exception: Благодаря чему мы понимаем, что исключение ValueError, которое было выброшено, возникло по причине другого исключения TypeError.
raise from None Однако, что же это за конструкция raise from None? Ведь не имеет смысла явно указывать пользователю, что исключение произошло без причины. В чем тогда будет отличие от обычного вызова raise, когда значение __cause__ инициализируется None? На самом деле, эта конструкция применяется для того, чтобы скрыть (подавить) остальной контекст исключения. Посмотрим на код ниже: try: raise ValueError except ValueError: raise TypeError from None При его запуске мы увидим уже совсем другую ошибку: Traceback (most recent call last): File "test.py", line 5, in raise TypeError from None TypeError В которой не содержится никакой информации об исходном исключении ValueError - оно было скрыто. Из-за чего так произошло? Дело в том, что при любой перезаписи атрибута __cause__ (даже значением None): e.__cause__ = None Происходит дополнительное действие: e.__suppress_context__ = True Этот магический атрибут __suppress_context__ отвечает за то, попадет ли в итоговый вывод значение __context__. В связи с чем, контекст всегда дополняется только одним значением - это либо значение атрибута __context__, либо, в случае, если была использована конструкция raise from, значение атрибута __cause__. В примере кода выше, при вызове конструкции raise TypeError from None, несмотря на то, что значение __context__ содержало ссылку на исключение ValueError, которое было самым первым в цепочке, оно было проигнорировано, поскольку перезапись атрибута __cause__ (оператором from None) установила __suppress_context__ в значение True.
Итоги Обобщив все вышесказанное мы можем понять, что: Атрибут __cause__ - содержит в себе прямую ссылку на то исключение, по причине которого оно возникло (from cause). Атрибут __context__ - используется при множественном вызове исключений и содержит в себе прямую ссылку на то исключение, которое возникло первым. Атрибут __suppress_context__ - отвечает за подавление значения __context__ и используется в конструкции raise from. И если говорить на более глобальном уровне, который виден при запуске кода: Конструкция raise from используется для того, чтобы сохранить дополнительный контекст исключения. Конструкция raise from None используется для того, чтобы показать только последнее исключение, и скрыть весь остальной контекст.
Дополнительно, можно почитать официальную документацию на этот счет.