引发似乎来自调用者的异常

raising an exception that appears to come from the caller

我有与 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 发送电子邮件说 "hey, your code is broken on line 17." 但实际上,LibraryFunction() 确切地知道它在发出异常时做了什么。 Alice 可以尽力重新表述异常,尽可能清楚地表明库被错误地 调用了 ,但是回溯 将注意力转移了 从这个事实。真正出错的地方是Bob代码的第4行。此外,爱丽丝的代码知道这一点,因此允许爱丽丝的代码将责任归咎于它所属的地方并不是权力的错位。因此,为了最大可能的透明度并减少支持流量,回溯不应深入到 Bob 代码的第 4 行,without Bob 不必自己编写此行为。

mattbornski 提供了一个 "you shouldn't be wanting to do this" 答案 here,我认为它遗漏了一个重点。当然,如果你说 "it's not my fault" 并推责,你不知道你一定是在推责到正确的地方。但是你确实知道你(LibraryFunction)已经努力对传递给你的输入参数进行显式类型检查,并且这个检查已经成功(在检查本身没有引发异常的感觉),结果是否定的。当然,从某种意义上说,Bob 的代码可能不是 "at fault",因为它可能没有生成无效输入——也许 Bob 只是从其他地方传递了那个参数。但不同的是,他没有检查就通过了。如果 Bob 努力检查,并且检查代码本身没有引发异常,那么 Bob 也应该放心 RaiseFromAbove,从而帮助他的代码的用户。

您可以重新定义函数 sys.excepthook,如 this answer:

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)

这需要 Python 3.5 用于 walk_tb

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

问题没有 good/direct/authoritative 答案。我在类别 "authoritative reference needed" 下发布了赏金,最接近的是 Martijn 的评论,即:

  1. 明确地做到这一点的方法是改变回溯 object 或生成一个新的回溯;
  2. 不能 在纯粹的 Python 中完成,但必须通过处理不受支持的 API 基础设施来完成;
  3. 这不值得。

我也这么怀疑。所以 unless/until 任何人实际上都可以提供权威参考来说明这种方法的不可能性,我会 post 在这里作为 "accepted" 答案。

但我不认为它对于 Python 来说不值得 wish-list。这个问题引起了相当多的 "you shouldn't be wanting to do this" 情绪,但我仍然不同意:

  • 当然,Bob 应该 学会正确阅读回溯,但是让他更容易 有什么不对?这样做——帮助 Alice 帮助他将注意力转移到正确的地方? Bob 天真到联系 Alice 并报告她代码中的错误的场景是一个夸大(尽管可能)的例子来说明这一点。更有可能的是,他只会有一个不必要的 2 秒停顿,因为他认为 "problem on line 17 of... oh wait, no, the caller is the problem"。但为什么不饶过他,让编程用户体验更流畅呢? Python 的理念在我看来就是围绕消除这种摩擦展开的。

  • 当然,任何推定的 RaiseFromAbove 都可能 被 Alice 不加区别地使用,或者以其他方式滥用,因此可能会让 Bob 更加困惑而不是更少。对我来说,这是一个虚假的论点,因为它同样适用于爱丽丝可能做出的任何其他不明智的编码决定,而且实际上适用于 Python 和其他语言中已经存在的许多强大功能。一把利器的价值应该根据它的价值来判断如果正确使用并遵守说明和安全警告。

无论如何,赏金截止日期临近,如果我什么都不做,我相信一半的赏金会用于 highest-voted 答案。目前这是 Tore 的,但对我来说,只加 1 让 Bob 做侦探工作的想法与我的目的相反:这让它看起来 more 好像有问题在爱丽丝的代码中。通过追查问题的智力练习,Bob 可能会成为更好的程序员,但他可能很着急,而且无论如何按照这种逻辑,我们都将在裸机上编程。所以我会奖励 yinnonsanders 的回答,不是因为它是一个完整且令人满意的解决方案,而是因为它至少符合问题的精神并且可能在某些情况下有效。