【问题标题】:Raise an exception from a higher level, a la warnings从更高级别引发异常,即警告
【发布时间】:2016-03-14 12:11:14
【问题描述】:

在模块警告 (https://docs.python.org/3.5/library/warnings.html) 中,可以引发似乎来自堆栈早期某处的警告:

warnings.warn('This is a test', stacklevel=2)

是否存在引发错误的等价物?我知道我可以使用替代回溯引发错误,但我无法在模块中创建该回溯,因为它需要来自更早的时间。我想像:

tb = magic_create_traceback_right_here()
raise ValueError('This is a test').with_traceback(tb.tb_next)

原因是我正在开发一个具有函数module.check_raise 的模块,我想引发一个似乎源自函数调用位置的错误。如果我在 module.check_raise 函数中引发错误,它似乎源自 module.check_raise,这是不受欢迎的。

另外,我尝试了一些技巧,比如引发一个虚拟异常、捕获它并传递回溯,但不知何故 tb_next 变成了None。我没有想法。

编辑:

我想要这个最小示例(称为 tb2.py)的输出:

import check_raise

check_raise.raise_if_string_is_true('True')

只有这样:

Traceback (most recent call last):
  File "tb2.py", line 10, in <module>
    check_raise.raise_if_string_is_true(string)
RuntimeError: An exception was raised.

【问题讨论】:

  • 嗯,如果您使用filter 将警告转换为错误然后调用warnings.warn 会成功吗?
  • source 判断它会raise 它,我只是不确定它是否会按照您希望的方式进行(看着它,我认为它不会) t)。
  • @Jim 我试过这个,它会引发错误,但它打印的回溯与您在该位置引发错误一样:stacklevel= 参数什么都不做。
  • Jinja2(一个模板引擎)为了完成类似的事情而使用了很多肮脏的技巧,但它需要大量的代码来实现。请参阅github.com/mitsuhiko/jinja2/blob/master/jinja2/debug.py 了解肮脏的详细信息,然后考虑您是否真的想走这条路...
  • @kiwidrew 是的,这看起来很糟糕。我不知道这是否可能。

标签: python python-3.x python-3.5


【解决方案1】:

我明白“不要这样做”。另一方面,我相信可能会有一些特殊的用例。我以这种方式产生了自己的错误(只是删除了一些定义的帧......)

def get_traceback_with_removed_frames_by_line_string(lines):
    """In traceback call stack, it is possible to remove particular level defined by some line content.

    Args:
        lines (list): Line in call stack that we want to hide.

    Returns:
        string: String traceback ready to be printed.
    """
    exc = trcbck.TracebackException(*sys.exc_info())
    for i in exc.stack[:]:
        if i.line in lines:
            exc.stack.remove(i)

    return "".join(exc.format())

我只返回字符串。

如果有具体的功能正在提升,您可以将其添加到忽略的帧中。

请记住,如果你隐藏某些东西,有人可能不明白为什么会发生某些事情......

我的用例是只隐藏第一级 - 我的库中用于装饰框架中所有用户功能的装饰器,因此来自用户端的错误位于 1 级。

【讨论】:

    【解决方案2】:

    我不敢相信我会发布这个

    这样做你违背了the zen

    特殊情况不足以打破规则。

    但如果你坚持这里就是你的神奇密码。

    check_raise.py

    ​​>
    import sys
    import traceback
    
    def raise_if_string_is_true(string):
        if string == 'true':
            #the frame that called this one
            f = sys._getframe().f_back
            #the most USELESS error message ever
            e = RuntimeError("An exception was raised.")
    
            #the first line of an error message
            print('Traceback (most recent call last):',file=sys.stderr)
            #the stack information, from f and above
            traceback.print_stack(f)
            #the last line of the error
            print(*traceback.format_exception_only(type(e),e),
                  file=sys.stderr, sep="",end="")
    
            #exit the program
            #if something catches this you will cause so much confusion
            raise SystemExit(1)
            # SystemExit is the only exception that doesn't trigger an error message by default.
    

    这是纯python,不会干扰sys.excepthook,即使在try块中也不会被except Exception:捕获,尽管它被except:捕获

    test.py

    ​​>
    import check_raise
    
    check_raise.raise_if_string_is_true("true")
    print("this should never be printed")
    

    会给你你想要的(可怕的缺乏信息和极度伪造的)追溯消息。

    Tadhgs-MacBook-Pro:Documents Tadhg$ python3 test.py
    Traceback (most recent call last):
      File "test.py", line 3, in <module>
        check_raise.raise_if_string_is_true("true")
    RuntimeError: An exception was raised.
    Tadhgs-MacBook-Pro:Documents Tadhg$
    

    【讨论】:

    • 我在运行清理时发现了这个。仅供参考,您的 sys._getframe() 调用被记录为内部和专用的,当前存在于 CPython 中,可能不存在于其他实现中。我相信这意味着sys 模块规范不需要此功能。从这个意义上说,它并不是真正的纯 Python。
    • 但是-我相信基本思想说“不要这样做”可能是这里最正确的答案。
    【解决方案3】:

    编辑:以前的版本没有提供引用或解释。

    我建议参考PEP 3134 动机中的说明:

    有时,故意让异常处理程序很有用 重新引发异常,以提供额外信息或 将异常转换为另一种类型。 __cause__ 属性 提供了一种明确的方式来记录异常的直接原因。

    当使用__cause__ 属性引发Exception 时,回溯消息采用以下形式:

    Traceback (most recent call last):
     <CAUSE TRACEBACK>
    
    The above exception was the direct cause of the following exception:
    
    Traceback (most recent call last):
      <MAIN TRACEBACK>
    

    据我了解,这正是您想要完成的;清楚地表明错误的原因不是您的模块,而是其他地方。如果您想像 edit 建议的那样尝试省略回溯的信息,那么这个答案的其余部分对您没有任何好处。


    只是语法说明:

    异常对象的__cause__ 属性总是被初始化 为无。它由一种新形式的“raise”语句设置:

       raise EXCEPTION from CAUSE
    

    相当于:

        exc = EXCEPTION
        exc.__cause__ = CAUSE
        raise exc
    

    所以最起码的例子应该是这样的:

    def function():
        int("fail")
    
    def check_raise(function):
        try:
            function()
        except Exception as original_error:
            err = RuntimeError("An exception was raised.")
            raise err from original_error
    
    check_raise(function)
    

    它会给出如下错误消息:

    Traceback (most recent call last):
      File "/PATH/test.py", line 7, in check_raise
        function()
      File "/PATH/test.py", line 3, in function
        int("fail")
    ValueError: invalid literal for int() with base 10: 'fail'
    
    The above exception was the direct cause of the following exception:
    
    Traceback (most recent call last):
      File "/PATH/test.py", line 12, in <module>
        check_raise(function)
      File "/PATH/test.py", line 10, in check_raise
        raise err from original_error
    RuntimeError: An exception was raised.
    

    然而第一行的原因是check_raisetry块中的语句:

      File "/PATH/test.py", line 7, in check_raise
        function()
    

    所以在提升err 之前,可能(或可能不)需要从original_error 中删除最外层的回溯帧:

    except Exception as original_error:
        err = RuntimeError("An exception was raised.")
        original_error.__traceback__ = original_error.__traceback__.tb_next
        raise err from original_error
    

    这样,回溯中唯一似乎来自check_raise 的行是最后一个raise 语句,纯python代码不能省略该语句,尽管取决于消息的信息量,您可以非常清楚地说明您的模块不是问题的原因:

    err = RuntimeError("""{0.__qualname__} encountered an error during call to {1.__module__}.{1.__name__}
    the traceback for the error is shown above.""".format(function,check_raise))
    

    像这样引发异常的好处是在引发新的错误时不会丢失原始的 Traceback 消息,这意味着可以引发一系列非常复杂的异常,python 仍然会正确显示所有相关信息:

    def check_raise(function):
        try:
            function()
        except Exception as original_error:
            err = RuntimeError("""{0.__qualname__} encountered an error during call to {1.__module__}.{1.__name__}
    the traceback for the error is shown above.""".format(function,check_raise))
            original_error.__traceback__ = original_error.__traceback__.tb_next
            raise err from original_error
    
    def test_chain():
        check_raise(test)
    
    def test():
        raise ValueError
    
    check_raise(test_chain)
    

    给我以下错误信息:

    Traceback (most recent call last):
      File "/Users/Tadhg/Documents/test.py", line 16, in test
        raise ValueError
    ValueError
    
    The above exception was the direct cause of the following exception:
    
    Traceback (most recent call last):
      File "/Users/Tadhg/Documents/test.py", line 13, in test_chain
        check_raise(test)
      File "/Users/Tadhg/Documents/test.py", line 10, in check_raise
        raise err from original_error
    RuntimeError: test encountered an error during call to __main__.check_raise
    the traceback for the error is shown above.
    
    The above exception was the direct cause of the following exception:
    
    Traceback (most recent call last):
      File "/Users/Tadhg/Documents/test.py", line 18, in <module>
        check_raise(test_chain)
      File "/Users/Tadhg/Documents/test.py", line 10, in check_raise
        raise err from original_error
    RuntimeError: test_chain encountered an error during call to __main__.check_raise
    the traceback for the error is shown above.
    

    是的,它很长,但它显着提供更多信息:

    Traceback (most recent call last):
      File "/Users/Tadhg/Documents/test.py", line 18, in <module>
        check_raise(test_chain)
    RuntimeError: An exception was raised.
    

    更不用说即使程序没有结束,原来的错误仍然可以使用:

    import traceback
    
    def check_raise(function):
        ...
    
    def fail():
        raise ValueError
    
    try:
        check_raise(fail)
    except RuntimeError as e:
        cause = e.__cause__
        print("check_raise failed because of this error:")
        traceback.print_exception(type(cause), cause, cause.__traceback__)
    
    print("and the program continues...")
    

    【讨论】:

    • 关闭-我实际上不想要最后一部分,“被检查的过程引发异常”。
    • 我假设您的意思是您不希望出现 raise x from e,但我不明白为什么如果部分错误特别指出 The above exception was the direct cause of the following exception: 无论如何我编辑了answer 解释答案并展示使用它的好处。
    • 只是一团糟。所有这一切背后的真正动机是我希望它对 git 模块进行版本检查。我编写脚本使用的模块。但我的模块有时会变化得相当快——它们不稳定。所以我制作了另一个模块(VCheck)来检查这些模块的 git 版本。如果它是正确的版本,那么脚本就会运行。如果不是,则 VCheck 应该引发异常。我希望异常是“版本错误”并回溯到脚本,而不是“VCheck 确定版本错误”并回溯到 VCheck。
    • 我注意到像 intopen 这样的内置函数按照你建议的方式运行,所以也许你只需要在 C 中创建你的函数:P
    • 是的,我想到了...不过这变得可笑地复杂了。我想我正在尝试做一些 Python 不打算做的事情......
    【解决方案4】:

    如果我理解正确,您会想要这个最小示例的输出:

    def check_raise(function):
        try:
            return function()
        except Exception:
            raise RuntimeError('An exception was raised.')
    
    def function():
        1/0
    
    check_raise(function)
    

    只有这样:

    Traceback (most recent call last):
      File "tb2.py", line 10, in <module>
        check_raise(function)
    RuntimeError: An exception was raised.
    

    事实上,它的输出要多得多;有异常链接,可以通过立即处理RuntimeError,删除它的__context__,并重新提升它来处理它,并且RuntimeError本身还有另一行回溯:

      File "tb2.py", line 5, in check_raise
        raise RuntimeError('An exception was raised.')
    

    据我所知,纯 Python 代码不可能在异常引发后替换异常的回溯;解释器可以控制添加它,但它只在处理异常时公开当前的回溯。没有用于将您自己的回溯传递给解释器的 API(甚至在使用跟踪函数时也没有),并且回溯对象是不可变的(这是涉及 C 级内容的 Jinja hack 解决的问题)。

    进一步假设您对缩短的回溯感兴趣,不是为了进一步的编程用途,而是为了用户友好的输出,您最好的选择是excepthook,它控制如何将回溯打印到控制台。为了确定在哪里停止打印,可以使用一个特殊的局部变量(这比将回溯限制为它的长度负 1 等更健壮一些)。此示例需要 Python 3.5(适用于 traceback.walk_tb):

    import sys
    import traceback
    
    def check_raise(function):
        __exclude_from_traceback_from_here__ = True
        try:
            return function()
        except Exception:
            raise RuntimeError('An exception was raised.')
    
    def print_traceback(exc_type, exc_value, tb):
        for i, (frame, lineno) in enumerate(traceback.walk_tb(tb)):
            if '__exclude_from_traceback_from_here__' in frame.f_code.co_varnames:
                limit = i
                break
        else:
            limit = None
        traceback.print_exception(
            exc_type, exc_value, tb, limit=limit, chain=False)
    
    sys.excepthook = print_traceback
    
    def function():
        1/0
    
    check_raise(function)
    

    这是现在的输出:

    Traceback (most recent call last):
      File "tb2.py", line 26, in <module>
        check_raise(function)
    RuntimeError: An exception was raised.
    

    【讨论】:

    • 这很好。是否可以将 sys.excepthook 行保留在模块内?我想将所有代码粘贴在一个模块中,这样它看起来就不同了。我会更新我的问题以更清楚。
    • 是的,这在技术上是可行的。但是,我建议将它放在模块内的初始化函数中,并在使用该模块的程序开始时调用一次。通过要求程序员有意识地初始化钩子,它变得不那么庞大(“显式优于隐式”),更重要的是,允许在进程初始化时以正确的顺序执行此类操作,以防其他模块摆弄相同的设施。否则,只要模块碰巧首先被导入,就会安装异常钩子。
    • 啊当...这可能是个问题吧?我不希望它完全霸占异常系统......也许它可以做一个测试以确保它只是“修复”我的模块生成的异常并让其他异常通过不受干扰......?
    • 这很简单。如果在回溯的任何帧中都没有找到标记名称,则它已经被原封不动地打印出来。唯一需要注意的是,如果回溯不是您的,则保持链接完整,即仅将 chain=False 传递给 print_exception 以用于您的异常。
    猜你喜欢
    • 2014-08-31
    • 1970-01-01
    • 1970-01-01
    • 2013-01-16
    • 1970-01-01
    • 2021-12-19
    • 2011-08-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多