【问题标题】:Detecting infinite recursion检测无限递归
【发布时间】:2013-04-11 17:53:48
【问题描述】:

我正在为 Trac 创建一个宏,它所做的其中一件事是渲染一些 wiki 文本,而这些文本又可以使用相同的宏。

如果使用相同的参数调用内部宏(即呈现相同的 wiki 文本位),这可能会导致无限递归。我想通过检查调用堆栈并在扩展宏的函数已经使用完全相同的一组参数调用时中断递归来阻止用户像这样射击自己的脚。

我一直在查看inspect module,这绝对是要走的路,但仍然无法弄清楚如何发现堆栈上前一个函数的参数值。我该怎么做?

【问题讨论】:

    标签: python recursion metaprogramming trac


    【解决方案1】:

    捕获递归异常是更好的方法,但您也可以在要“保护”的函数上添加装饰器:

    from functools import wraps
    from threading import local
    
    def recursion_detector(func):
        func._thread_locals = local()
    
        @wraps(func)
        def wrapper(*args, **kwargs):
            params = tuple(args) + tuple(kwargs.items())
    
            if not hasattr(func._thread_locals, 'seen'):
                func._thread_locals.seen = set()
            if params in func._thread_locals.seen:
                raise RuntimeError('Already called this function with the same arguments')
    
            func._thread_locals.seen.add(params)
            try:
                res = func(*args, **kwargs)
            finally:
                func._thread_locals.seen.remove(params)
    
            return res
    
        return wrapper
    

    然后将该装饰器应用于宏渲染函数。

    一个简单的演示:

    >>> @recursion_detector
    ... def foo(bar):
    ...     return foo(not bar)
    ... 
    >>> foo(True)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 10, in wrapper
      File "<stdin>", line 3, in foo
      File "<stdin>", line 10, in wrapper
      File "<stdin>", line 3, in foo
      File "<stdin>", line 7, in wrapper
    RuntimeError: Already called this function with the same arguments
    >>> foo(False)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 10, in wrapper
      File "<stdin>", line 3, in foo
      File "<stdin>", line 10, in wrapper
      File "<stdin>", line 3, in foo
      File "<stdin>", line 7, in wrapper
    RuntimeError: Already called this function with the same arguments
    

    【讨论】:

    • 太好了,谢谢!对于未来的读者,this post 提供了关于 threading.local 工作原理的很好的解释和示例。
    【解决方案2】:

    在递归错误发生时捕获它比在运行时尝试在它发生之前捕获它更容易。

    如果这不是一个选项,那么在渲染之前分析模板也可能是一种前进的方式。

    【讨论】:

    • 如果代码不会花费太长时间来获得堆栈溢出。如果是这样,这可能是不切实际的。 (+1)
    • @NPE:当然,如果这是一个非常缓慢的过程。但是不应该渲染模板。
    • 这是我的第一次尝试,但由于某种我仍然无法理解的原因,发生这种情况时不会引发异常。当我通过 {{tracd}} 运行 trac 时引发此问题时,该过程只是...停止。没有调试消息。没有例外。什么都没有。
    • @LennartRegebro 肯定......虽然我能够确定它在第 97 次迭代时失败了,并使用调试器单步执行代码以查看失败的位置。奇怪的是,这是一个函数返回的时候(我原以为它正在调用),它并没有在我最近的 try .. except 块中的断点处停止。
    【解决方案3】:

    同样简单的是传递字典来跟踪使用的参数,并在开始时检查参数是否已经尝试过。

    【讨论】:

    • 没那么简单,因为我用的是trac的API。要添加宏,我需要重写 IWikiMacroProvider 提供的方法 expand_macro(self, formatter, name, content)。不是这个方法直接调用自己,而是在我对Chrome(self.env).render_template(...) 的调用中的某个地方。
    • 我猜@MartijnPieters 的回答与您的建议接近。
    猜你喜欢
    • 2012-03-26
    • 1970-01-01
    • 2014-08-17
    • 2012-02-20
    • 1970-01-01
    • 1970-01-01
    • 2018-06-22
    • 2011-01-31
    • 1970-01-01
    相关资源
    最近更新 更多