【问题标题】:How can I elide a function wrapper from the traceback in Python-3?如何从 Python-3 的回溯中删除函数包装器?
【发布时间】:2017-12-02 11:00:06
【问题描述】:

问题

幽灵的威胁

假设我写了一个函数装饰器,它接受该函数,并将其包装在另一个函数中,如下所示:

# File example-1.py
from functools import wraps

def decorator(func):
    # Do something
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Do something
        return func(*args, **kwargs)
        # Do something
    # Do something
    return wrapper

现在假设我正在装饰的函数引发异常:

@decorator
def foo():
    raise Exception('test')

运行foo() 的结果将打印出以下回溯(在任何 Python 版本中):

Traceback (most recent call last):
  File "./example-1.py", line 20, in <module>
    foo()
  File "./example-1.py", line 11, in wrapper
    return func(*args, **kwargs)
  File "./example-1.py", line 18, in foo
    raise Exception('test')
Exception: test

克隆人的攻击

好的,现在我查看我的回溯,我看到它通过了wrapper 函数。如果我多次包装函数会怎样(可能使用稍微复杂一点的装饰器对象,它在其构造函数中接收参数)?如果我经常在我的代码中使用这个装饰器(我将它用于日志记录、分析或其他)怎么办?

Traceback (most recent call last):
  File "./example-1.py", line 20, in <module>
    foo()
  File "./example-1.py", line 11, in wrapper
    return func(*args, **kwargs)
  File "./example-1.py", line 11, in wrapper
    return func(*args, **kwargs)
  File "./example-1.py", line 11, in wrapper
    return func(*args, **kwargs)
  File "./example-1.py", line 11, in wrapper
    return func(*args, **kwargs)
  File "./example-1.py", line 18, in foo
    raise Exception('test')
Exception: test

当我从函数定义中知道包装器存在时,我不希望它“污染”我的回溯,并且当它显示的代码 sn-p 无用时,我不希望它多次显示@ 987654329@

Python 2

西斯的复仇

在 Python-2 中,正如 this answer 对另一个问题所指出的那样,以下技巧可以完成这项工作:

# In file example-2.py

def decorator(func):
    # Do something
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Do something
        info = None
        try:
            return func(*args, **kwargs)
        except:
            info = sys.exc_info()
            raise info[0], info[1], info[2].tb_next
        finally:
            # Break the cyclical reference created by the traceback object
            del info
        # Do something
    # Do something
    return wrapper

通过使用此习惯用法直接将对包装函数的调用包装在与我想从回溯中删除的函数相同的块中,我有效地从回溯中删除了当前层并让异常继续传播。每次堆栈展开通过此函数时,它都会将自身从回溯中移除,因此此解决方案可以完美运行:

Traceback (most recent call last):
  File "./example-2.py", line 28, in <module>
    foo()
  File "./example-2.py", line 26, in foo
    raise Exception('test')
Exception: test

(但请注意,您不能将此习惯用法封装在另一个函数中,因为堆栈将从该函数展开回wrapper,它仍将被添加到回溯中)

Python 3

新希望

现在我们已经介绍了这一点,让我们继续讨论 Python-3。 Python-3 引入了这种新语法:

raise_stmt ::=  "raise" [expression ["from" expression]]

允许使用新异常的__cause__ 属性链接异常。这个特性对我们来说是无趣的,因为它修改了异常,而不是回溯。我们的目标是成为一个完全透明的包装器,就可见性而言,这是行不通的。

或者,我们可以尝试以下语法,promises to do 是我们想要的(代码示例取自 python 文档):

raise Exception("foo occurred").with_traceback(tracebackobj)

使用这种语法,我们可以尝试这样的事情:

# In file example-3
def decorator(func):
    # Do something
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Do something
        info = None
        try:
            return func(*args, **kwargs)
        except:
            info = sys.exc_info()
            raise info[1].with_traceback(info[2].tb_next)
        finally:
            # Break the cyclical reference created by the traceback object
            del info
        # Do something
    # Do something
    return wrapper

帝国反击

但是,不幸的是,这并没有达到我们想要的效果:

Traceback (most recent call last):
  File "./example-3.py", line 29, in <module>
    foo()
  File "./example-3.py", line 17, in wrapper
    raise info[1].with_traceback(info[2].tb_next)
  File "./example-3.py", line 27, in foo
    raise Exception('test')
Exception: test

如您所见,执行raise 语句的行显示在回溯中。这似乎来自这样一个事实,虽然 Python-2 语法将第三个参数的回溯设置为 raise,因为函数正在展开,因此它没有添加到回溯链中(如数据下的文档中所述Model),另一方面,Python-3 语法将Exception 对象上的回溯更改为函数上下文中的表达式,然后将其传递给raise 语句,该语句将代码中的新位置添加到回溯链(在 Python-3 中对此的解释非常相似)。

想到的解决方法是避免使用"raise" [ expression ] 形式的语句,而是使用干净的raise 语句让异常像往常一样传播,但手动修改异常对象__traceback__ 属性:

# File example-4
def decorator(func):
    # Do something
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Do something
        info = None
        try:
            return func(*args, **kwargs)
        except:
            info = sys.exc_info()
            info[1].__traceback__ = info[2].tb_next
            raise
        finally:
            # Break the cyclical reference created by the traceback object
            del info
        # Do something
    # Do something
    return wrapper

但这根本不起作用!

Traceback (most recent call last):
  File "./example-4.py", line 30, in <module>
    foo()
  File "./example-4.py", line 14, in wrapper
    return func(*args, **kwargs)
  File "./example-4.py", line 28, in foo
    raise Exception('test')
Exception: test

绝地归来(?)

那么,我还能做什么?由于语法的变化,似乎使用“传统”的方式是行不通的,而且我不想在项目级别开始弄乱回溯打印机制(使用traceback 模块) .这是因为如果不是不可能的话,很难在可扩展中实现,它不会破坏任何其他尝试更改回溯、在顶层以自定义格式打印回溯或以其他方式执行任何其他操作的包与问题相关。

另外,有人能解释一下为什么最后一种技术实际上完全失败了吗?

(我在 python 2.6、2.7、3.4、3.6 上尝试了这些示例)

编辑:经过一段时间的思考,在我看来,python 3 的行为更有意义,以至于 python 2 的行为几乎看起来像一个设计错误,但我仍然认为应该有办法做这种事。

【问题讨论】:

  • 不完全有资格理解这个问题,但 +1 的努力:)
  • 谢谢! ^_^ 我已经跟踪这个问题大约一周了。

标签: python python-3.x decorator python-2.x traceback


【解决方案1】:

简单的答案是您不应该这样做。从追溯中隐藏事物是危险的。您可能认为您不想显示该行,因为它是微不足道的或“只是一个包装器”,但一般来说,如果它没有做某事,您就不会编写包装器函数。接下来你知道包装函数中有一个错误现在无法找到,因为包装函数已经从回溯中删除了自己。

只需处理回溯中的额外行,或者,如果您真的需要,覆盖 sys.excepthook 并在顶层过滤掉它们。如果您担心其他人也会覆盖sys.excepthook,那么将您的所有代码包装在一个执行异常打印本身的顶级函数中。从回溯中隐藏关卡不是也不应该容易。

【讨论】:

  • 如果您查看我输入的# Do something cmets,我专门将它们放在建议的习语周围,以便像往常一样显示任何其他异常。我只对在调用微不足道时跳过包装器感兴趣。
  • 如果我使用sys.excepthook,当我特别忽略它时,我会失去对它的细粒度控制,因为如果包装器也调用其他函数,那么区分包装函数中何时发生异常并非易事当它发生在另一个函数中时。
  • 隐藏部分回溯是危险的,但在极少数情况下,您可能会认为这确实是值得的。例如,importlib 有自己的 ugly hacks,因此当导入期间出现问题时,importlib 帧通常不会出现在回溯中。
  • @Re.po:即使那条线是微不足道的,但这并不意味着抑制整个框架是个好主意。例如,装饰器可以在前面的行中更改参数甚至被调用的函数,以便调用行引发的异常可能在包装器的其他地方有其真正的原因。如果整个帧被抑制,则无法看到这一点。也就是说,如果您在 Google 周围搜索,您会看到有人建议模仿 jinja2,这有效地重新实现了回溯对象。
  • 我并不是要粗鲁,但我特别指的是我既不改变func也不改变argskwargs的情况。至于 jinja2,这正是我想要避免的那种过度实现。我更喜欢实现尽可能简单和直接。我有一个涉及sys.excepthook 的想法,它将使用辅助模块并且仍然不会干扰其他代码(只需用其他东西包装原始函数)。如果我弄清楚并有时间写它,我稍后会发布一个带有这个想法的编辑。
【解决方案2】:

内部_testcapi 模块包含PyErr_SetExcInfo 函数的绑定,这使得它相当容易实现。仅在 cpython 3.9 上测试过;不能保证它会在未来继续工作。

import functools
import typing as T

A_Callable = T.TypeVar("A_Callable", bound=T.Callable[..., T.Any])

try:
    from sys import exc_info
    from _testcapi import set_exc_info
    def silent_wrapper(f: A_Callable) -> A_Callable:
        @functools.wraps(f)
        def wrapper(*args: T.Any, **kwargs: T.Any) -> T.Any:
            try:
                return f(*args, **kwargs)
            except:
                tp, exc, tb = exc_info()
                # Drop two frames, one for silent_wrapper itself and one for the
                # wrapped function
                set_exc_info(tp, exc, tb.tb_next.tb_next) # type: ignore[union-attr]
                del tp, exc, tb
                raise
        return T.cast(A_Callable, wrapper)

except ImportError:
    def silent_wrapper(f: A_Callable) -> A_Callable:
        return f

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-01-14
    • 1970-01-01
    • 2017-10-08
    • 2019-08-08
    • 2010-11-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多