【问题标题】:raising an exception that appears to come from the caller引发似乎来自调用者的异常
【发布时间】:2017-09-16 04:44:33
【问题描述】:

我有与here 相同的问题,但错误地关闭了 作为another related question的副本:

Python 库如何以它自己的方式引发异常? 它没有在回溯中公开的代码?动机是实现它 清楚库函数被错误地调用:违规 来电者的线路应该看起来应该承担责任,而不是 图书馆内(故意和正确地)提出的行 例外。

正如 Ian 在对已结束问题的评论中指出的那样,这不是 就像问你如何调整调用者中的代码来改变 回溯的出现方式。

我失败的尝试如下。 在标记为QUESTION 的行,我尝试修改 tb,例如tb.tb_frame = tb.tb_frame.f_back 但这会导致 AttributeError: readonly attribute。我也试图创建一个 具有与tb 相同属性的鸭子类型对象,但在 reraise(),与TypeError: __traceback__ must be a traceback or None。 (我试图通过子类化traceback 来战胜它,但遇到了TypeError: type 'traceback' is not an acceptable base type)。

调整traceback 对象本身在任何情况下都可能是这个X 的错误Y - 也许还有其他策略?

假设 Alice 编写了以下库:

import sys

# home-made six-esque Python {2,3}-compatible reraise() definition
def reraise( cls, instance, tb=None ): # Python 3 definition
    raise ( cls() if instance is None else instance ).with_traceback( tb )
try: 
    Exception().with_traceback
except: # fall back to Python 2 definition
    exec( 'def reraise( cls, instance, tb=None ): raise cls, instance, tb' )
    # has to be wrapped in exec because this would be a syntax error in Python 3.0

def LibraryFunction( a ):
    if isinstance( a, (int, float) ):
        return a + 1
    else:
        err = TypeError( "expected int or float, got %r" % a )
        RaiseFromAbove( err )   # the traceback should NOT show this line
                                # because this function knows that it is operating
                                # correctly and that the caller is at fault

def RaiseFromAbove( exception, levels=1 ):
    # start by raising and immediately catching the exception
    # so that we can get a traceback from sys.exc_info()
    try:
        raise( exception )
    except:  
        cls, instance, tb = sys.exc_info()
        for i in range( levels + 1 ):
            pass # QUESTION: how can we manipulate tb here, to remove its deepest levels?
        reraise( cls, instance, tb )

现在,假设 Alice 发布了库,而 Bob 下载了它。 Bob 编写了如下调用它的代码:

from AlicesLibrary import LibraryFunction

def Foo():
    LibraryFunction( 'invalid input' )  # traceback should reach this line but go no deeper

Foo()

关键是,由于没有有效的RaiseFromAbove,因此回溯将显示异常源自 Alice 库的第 17 行。因此,Bob(或其中一个重要的 Bob 子群)将给 Alice 发送电子邮件说“嘿,你的代码在第 17 行被破坏了。”但事实上,LibraryFunction() 确切地知道它在发出异常时做了什么。 Alice 可以尽最大努力重新表述异常,以尽可能清楚地表明该库被错误地调用,但回溯将注意力从这一事实中移开。实际犯错的地方是 Bob 代码的第 4 行。此外,Alice 的代码知道这一点,因此允许 Alice 的代码将责任归咎于它所属的地方并不是权力错位。因此,为了尽可能提高透明度并减少支持流量,回溯的深度不应超过 Bob 代码的第 4 行,Bob 不必自己编写此行为的代码。

mattbornski 提供了一个“你不应该这样做”的答案here,我认为这忽略了一个重要的点。当然,如果你说“这不是我的错”并推卸责任,你不知道你一定是把责任推到了正确的地方。但是您确实知道您 (LibraryFunction) 已经努力对您收到的输入参数进行显式类型检查,并且该检查已成功(从某种意义上说,检查本身没有引发异常),结果为负。当然,Bob 的代码可能没有“错误”,因为它可能没有生成无效输入——也许 Bob 只是从其他地方传递了这个参数。但不同的是,他在没有检查的情况下通过了它。如果 Bob 努力进行检查,并且检查代码本身没有引发异常,那么 Bob 也应该随时联系RaiseFromAbove,从而帮助他的代码的用户。

【问题讨论】:

  • IMO 你不能。如果您可以控制引用的打印方式,则可以隐藏一些帧,参考:mail.python.org/pipermail/python-list/2012-October/632386.html
  • 将路径反映到您的库中的回溯到底有什么问题?如果没有完整的回溯,您将如何调试自己的库代码中的错误?如果调用者传入了错误的数据,您可以提前验证并抛出异常early,但仍在您的库中。但是不要仅仅因为您不相信使用您的库的开发人员知道如何读取回溯而诉诸黑客攻击。
  • 异常不应归咎于责备——引发异常有很多正当理由。我不认为非初学者 Python 开发人员通常认为异常 = 库中的错误。如果您提出信息异常,开发人员应该能够理解。
  • 如果可能的话,它可以掩盖库代码中的真正错误。这样的错误功能会使图书馆很快变得非常不受欢迎。只需记录您的库函数验证其输入并引发明显的自定义异常 - InvalidArgumentsError 或类似 - 如果它们无效。
  • 您为什么不正确地记录您的图书馆?我的意思是,如果 bob 想要使用开源库并且既不阅读代码也不阅读文档,这意味着键盘和椅子之间存在问题。如果鲍勃读到“无效输入会引发异常”之类的内容,我认为鲍勃不会责怪爱丽丝。

标签: python exception exception-handling


【解决方案1】:

这个问题没有好的/直接/权威的答案。我在“需要权威参考”类别下发布了赏金,最接近的是 Martijn 的 cmets,即:

  1. 确定的方法是更改​​回溯对象或生成一个新对象;
  2. 这个不能在纯 Python 中完成,但必须通过使用不受支持的 API 基础设施来完成;
  3. 这不值得。

我也有同样的怀疑。因此,除非/直到任何人实际上都可以提供有关这种方法不可能性的权威参考,否则我将在此处将其发布为“已接受”的答案。

但我不接受它不是 Python 的一个值得的愿望清单项目。这个问题产生了相当多的“你不应该想要这样做”的情绪,我仍然不同意:

  • 当然,Bob 应该学会正确阅读回溯,但是让他更容易这样做有什么问题 - 帮助 Alice 帮助他指导他的注意正确的地方? Bob 天真地联系 Alice 并报告她的代码中的错误的场景是一个夸大了(尽管可能)的例子,以说明这一点。更有可能的是,他只是有一个不必要的 2 秒停顿,因为他认为“第 17 行的问题......哦等等,不,调用者是问题”。但是为什么不放过他,让编程 UX 更流畅呢?在我看来,Python 的哲学似乎围绕着消除这种摩擦。

  • 当然,任何推定的 RaiseFromAbove 都可能被 Alice 不加选择地使用或以其他方式滥用,因此可能会让 Bob 更加困惑而不是更少。对我来说,这是一个虚假的论点,因为它同样适用于 Alice 可能做出的任何其他不明智的编码决定,实际上也适用于 Python 和其他语言中已经存在的许多强大功能。锋利工具的价值应根据其价值判断正确使用是否符合说明和安全警告。

无论如何,赏金截止日期即将到来,如果我什么都不做,我相信一半的赏金会流向最高投票的答案。目前这是 Tore 的,但对我来说,只加 1 并让 Bob 做侦探工作的想法与我的想法相反:这使得它看起来 更像 Alice 的代码中存在问题. Bob 可能会从跟踪问题的智力练习中成为一个更好的程序员,但他可能很着急,无论如何按照这种逻辑,我们都会在裸机上进行编程。所以我将赏金奖励给 yinnonsanders 的回答,不是因为它是一个完整且令人满意的解决方案,而是因为它至少符合问题的精神,并且可能在某些情况下有效。

【讨论】:

    【解决方案2】:

    你可以像this answer一样重新定义函数sys.excepthook

    import os
    import sys
    import traceback
    
    def RaiseFromAbove( exception ):
        sys.excepthook = print_traceback
        raise( exception )
    
    def print_traceback(exc_type, exc_value, tb):
        for i, (frame, _) in enumerate(traceback.walk_tb(tb)):
            # for example:
            if os.path.basename(frame.f_code.co_filename) == 'AlicesLibrary.py':
                limit = i
                break
        else:
            limit = None
        traceback.print_exception(exc_type, exc_value, tb, limit=limit, chain=False)
    

    walk_tb 需要 Python 3.5

    【讨论】:

    • 嗯,我可以看到这对于非交互式单线程 Python 应用程序来说是一种可行的方法,其中异常对整个进程来说是致命的。对于其他情况,存在持久的、全局更改的sys.excepthook 的副作用。这对其他任何定制了钩子的人(例如 IPython)都不好。
    • 这有点矫枉过正,你可以设置一个不同的回溯对象。
    【解决方案3】:

    我认为你的 X 的 Pythonic Y 会改变你的代码,以便失败本身引发异常。在这种情况下,与其进行显式类型检查然后调用异常引发方法,不如练习 EAFP,只需将 1 添加到它们传入的任何内容中,如果它不起作用则让它引发错误。如果出于某种原因需要明确限制这两种类型,请将assert isinstance(a, (int, float)), "LibraryFunction() requires an int or float argument" 添加到函数的开头,并让类型验证引发错误。如果 Alice 的库中堆栈跟踪底部的代码行不是异常的“真实”源,请不要尝试隐藏该代码行,重组代码本身,以便堆栈跟踪告诉您实际发生了什么错误的。这将使 Alice 的代码更易于维护,并且 Bob 可以将堆栈追回到他自己的代码中,以查看他的问题是从哪里开始的。

    【讨论】:

      猜你喜欢
      • 2011-05-02
      • 1970-01-01
      • 2020-01-08
      • 1970-01-01
      • 2012-05-05
      • 2015-08-14
      • 1970-01-01
      • 1970-01-01
      • 2012-05-23
      相关资源
      最近更新 更多