如何从Python-3的追溯中删除函数包装器? [英] How can I elide a function wrapper from the traceback in Python-3?

查看:78
本文介绍了如何从Python-3的追溯中删除函数包装器?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Say i wrote a function decorator which takes the function, and wraps it in another function like so:

# 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

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

Now lets suppose the function I'm decorating raises an exception:

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

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

The result of running foo() will print out the following traceback (In any Python version):

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



克隆人的攻击



好的,现在我看一下回溯,发现它通过了包装器函数。如果我多次包装该函数(大概是一个稍微复杂一些的装饰器对象,该对象在其构造函数中接收参数)怎么办?如果我经常在代码中使用此装饰器(我将其用于日志记录,性能分析或其他操作)怎么办?

Attack of the Clones

OK, now i look at my traceback and i see it goes through the wrapper function. What if I wrapped the function multiple times(presumably with a slightly more sophisticated decorator object which receives arguments in its constructor)? What if I use this decorator often in my code(I use it for logging, or profiling, or whatever)?

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

当我从函数定义中知道包装器存在时,我不希望它污染我的回溯,并且我不希望它在代码片段中多次出现显示的是无用的返回函数(* args,** kwargs)

I don't want it "polluting" my traceback when i know from the function definition that the wrapper is there, and i don't want it showing up multiple times when the code snippet it displays is the unhelpful return func(*args, **kwargs)

在Python-2中,为此答案针对另一个问题指出,以下技巧可以完成工作:

In Python-2, as this answer to a different question points out, the following trick does the job:

# 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

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

By directly wrapping the call to the wrapped function with this idiom in the same block as the function I want to elide from the traceback, I effectively remove the current layer from the traceback and let the exception keep propagating. Every time the stack unwinding goes through this function, it removes itself from the traceback so this solution works perfectly:

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

(但是请注意,您不能将此成语封装在另一个函数中,因为堆栈会尽快从该函数退回到包装器,它仍将添加到回溯中)

(Note however that you can not encapsulate this idiom in another function, since as soon the stack will unwind from that function back into wrapper, it will still be added to the traceback)

现在我们已经涵盖了这一点,让我们继续使用Python-3。 Python-3引入了这种新语法:

Now that we have this covered, lets move along to Python-3. Python-3 introduced this new syntax:

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

允许使用以下属性的 __ cause __ 属性链接异常新的例外。这个功能对我们来说并不有趣,因为它修改了异常,而不是回溯。就可见性而言,我们的目标是成为一个完全透明的包装程序,这样就不会做。

which allows chaining exceptions using the __cause__ attribute of the new exception. This feature is uninteresting to us, since it modifies the exception, not the traceback. Our goal is to be a completely transparent wrapper, as far as visibility goes, so this won't do.

或者,我们可以尝试以下语法,其中我们要做的承诺(取自python文档的代码示例):

Alternatively, we can try the following syntax, which promises to do what we want (code example taken from the python documentation):

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

使用此语法,我们可以尝试以下操作:

Using this syntax we may try something like this:

# 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



帝国反击战



但是,不幸的是,这并不能满足我们的要求:

The Empire Strikes Back

But, unfortunately, this does not do what we want:

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 ,所以未将其添加到追溯链(如数据模型下的文档所述),另一方面,Python-3语法将 Exception 对象上的追溯更改为函数上下文中的表达式,然后将其传递给 raise 语句,该语句将代码中的新位置添加到追溯链中(在Python-3中对此的解释非常相似)。

As you can see, the line executing the raise statement shows up in the traceback. This seems to come from the fact that while the Python-2 syntax sets the traceback from the third argument to raise as the function is being unwound, and thus it is not added to the traceback chain(as explained in the docs under Data Model), the Python-3 syntax on the other hand changes the traceback on the Exception object as an expression inside the functions context, and then passes it to the raise statement which adds the new location in code to the traceback chain (the explanation of this is very similar in Python-3).

想到的一种解决方法是避免使用语句的 raise [expression] 形式,而应使用clean raise 语句使异常照常传播,但手动修改异常对象 __ traceback __ 属性:

A workaround that comes to mind is avoiding the "raise" [ expression ] form of the statement, and instead use the clean raise statement to let the exception propagate as usual but modify the exception objects __traceback__ attribute manually:

# 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 模块)。这是因为很难,即使不是不可能以可扩展的方式实现,也不会破坏任何其他试图更改回溯,在顶层以自定义格式打印回溯的软件包,或进行其他任何操作与问题有关。

Return of the Jedi(?)

So, what else can i do? It seems like using the "traditional" way of doing this just won't work because of the change in syntax, and I wouldn't want to start messing with the traceback printing mechanism (using the traceback module) at the project level. This is because it'll be hard if not impossible to implement in an extensible which won't be disruptive to any other package that tries to change the traceback, print the traceback in a custom format at the top level, or otherwise do anything else related to the issue.

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

Also, can someone explain why in fact the last technique fails completely?

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

(I tried these examples on python 2.6, 2.7, 3.4, 3.6)

编辑:考虑了一段时间后,我认为python 3的行为更有意义,到了python 2的行为几乎看起来像是一个设计错误的地步,但是我仍然认为应该有一种方法可以做到这一点。

After thinking about it for a while, in my opinion the python 3 behavior makes more sense, to the point that the python 2 behavior almost looks like a design bug, but I still think that there should be a way to do this kinda stuff.

推荐答案

简单的答案是您不应该这样做。隐藏来自追溯的东西是危险的。您可能会认为您不希望显示该行,因为它是琐碎的或只是包装程序,但是一般来说,如果它没有做任何事情,就不会编写该包装程序功能。接下来您会知道包装器函数中存在一个错误,由于该包装器函数已将其自身从跟踪中删除,因此该错误现在无法找到。

The simple answer is that you shouldn't do that. Hiding things from the traceback is dangerous. You may think you don't want to show that line because it's trivial or "just a wrapper", but in general you wouldn't write the wrapper function if it didn't do something. Next thing you know there is a bug in the wrapper function which is now unfindable because the wrapper function has erased itself from the traceback.

只需处理追溯,或者,如果您确实需要,请覆盖 sys.excepthook 并将其过滤掉。如果您担心其他人也会覆盖 sys.excepthook ,则将所有代码包装在一个顶层函数中,该函数会自行打印异常。隐藏和隐藏回溯中的关卡并非易事。

Just deal with the extra lines in the traceback, or, if you really want, override sys.excepthook and filter them out at the top level. If you're worried about someone else overriding sys.excepthook too, then wrap all your code in a top-level function that does the exception printing itself. It isn't and shouldn't be easy to hide levels from the traceback.

这篇关于如何从Python-3的追溯中删除函数包装器?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆