【问题标题】:Python 2 and 3 compatible way of hiding a decorator from stacktrace从堆栈跟踪中隐藏装饰器的 Python 2 和 3 兼容方式
【发布时间】:2025-12-23 05:50:11
【问题描述】:

我写了一个不想出现在堆栈跟踪中的装饰器。所以在 Python2 中我会这样做:

class SneakyDecorator(object):
    def __init__(self, f):
        self.f = f

    def __call__(self, *args, **kwargs):
        # [...]
        try:
            self.f(*args, **kwargs)
        except:
            t, v, tb = sys.exc_info()
            raise v, None, tb.tb_next             # <=== Important line

在 Python3 中我会这样做:

class SneakyDecorator:
    def __init__(self, f):
        self.f = f

    def __call__(self, *args, **kwargs):
        # [...]
        try:
            self.f(*args, **kwargs)
        except:
            t, v, tb = sys.exc_info()
            raise v.with_traceback(tb.tb_next)   # <=== Important line

所以问题来了:有什么方法可以同时兼容 Python2 和 Python3? 我更喜欢不涉及两个独立代码库的解决方案。

我尝试使用six 模块的reraise 函数,但问题是,该函数随后出现在堆栈跟踪中。

如果你在这个答案中这样做,同样的问题:Exception with original traceback - 2.6-3.X compatible version

更新:Python3 代码不起作用!调用方法仍然显示在堆栈跟踪中。所以接下来的问题是:有没有办法在 Python3 中做到这一点?

【问题讨论】:

  • 为什么要降低堆栈跟踪的准确性?
  • 这只是一个问题;这里的问题是如何以跨 Python 兼容的方式引发具有给定回溯的异常。从其他帖子中不清楚如何做到这一点?
  • 您将无法让您的所有项目交叉兼容。这只是徒劳的。
  • @AlexThornton:所以我们应该放弃吗?那是一种……非生产性的态度。当 PyPI 上可用的框架或库同时兼容 Python 2 和 3 时,我真的很感激。
  • 我同意链接线程的答案几乎与它得到的一样好。您在 py2 中唯一的选择是在 py3 中使用 SyntaxError,因此您基本上必须做一些 if PY2: exec(...)hackery。

标签: python python-2.7 python-3.x decorator stack-trace


【解决方案1】:

实现这项工作的唯一方法是保持__call__ 方法的不同版本,因为exec 本身会创建一个堆栈帧!以下,在 Python 2 中再次揭示了框架:

class SneakyDecorator:
    def __init__(self, f):
        self.f = f

    def __call__(self, *args, **kwargs):
        # [...]
        try:
            self.f(*args, **kwargs)
        except:
            t, v, tb = sys.exc_info()
            exec("raise v, None, tb.tb_next")

结果:

>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 12, in __call__
  File "<stdin>", line 3, in f
ValueError: Oi!

所以使用:

def __call__(self, *args, **kwargs):
    # [...]
    try:
        self.f(*args, **kwargs)
    except:
        t, v, tb = sys.exc_info()
        if sys.version_info[0] <= 2:
            exec("raise v, None, tb.tb_next")
        else:
            raise v.with_traceback(tb.tb_next)

实际上不会起作用。

对于 Python 2,以下内容与您的两个单独的类具有完全相同的功能:

if sys.version_info[0] <= 2:
    _call = '''\
def _call(self, *args, **kwargs):
    # [...]
    try:
        self.f(*args, **kwargs)
    except:
        t, v, tb = sys.exc_info()
        raise v, None, tb.tb_next
'''
    exec(_call)
else:
    def _call(self, *args, **kwargs):
        # [...]
        try:
            self.f(*args, **kwargs)
        except:
            t, v, tb = sys.exc_info()
            raise v.with_traceback(tb.tb_next)


class SneakyDecorator:
    def __init__(self, f):
        self.f = f

    __call__ = _call
    # niceties, patch up name and qualified name. Optional.
    __call__.__name__ = '__call__'
    __call__.__qualname__ = 'SneakyDecorator.__call__'

但是,请注意,对于 Python 3,Exception.with_traceback() 方法可能允许您附加一个新的回溯并删除当前帧,但 Python 会在您重新引发时将其重新添加例外!

【讨论】:

  • 我试过这个,但是在 Python2 中,exec 函数出现在堆栈跟踪中。
  • 嗯,好的。所以看起来这在 Python3 中是不可能的。这有点可悲。