【问题标题】:Is python maximum recurrence depth per object?python是每个对象的最大重复深度吗?
【发布时间】:2016-12-11 05:08:52
【问题描述】:

我正在重构 Python 信号处理框架,因为我们遇到了最大重复深度错误。

对该错误最可能的解释是,有时单个类的大量实例是从类的 init 方法递归创建的。事实上,模拟这种情况的实验会导致异常 RuntimeError: 'maximum recursion depth exceeded'。

当我将链中的下一个元素创建到管理这些对象的容器时,问题似乎消失了,尽管在我天真的理解中构建了相同深度的调用堆栈。我通过所有以前创建的对象调用创建。 (见示例代码)。

我倾向于得出结论,递归深度限制是以某种方式为每个对象设置的(无论是类还是实例)。当通过 init 递归创建时,可能会将所有内容放在类的堆栈中,就好像我们通过同一类的一系列实例递归创建它们一样,所有对象的递归深度都非常有限。

我正在为这个假设寻求一些证实,但发现很难得到证实或反驳。

import sys

class Chunk(object):
    pre=None
    post=None
    def __init__(self, container,pre):
        print 'Chunk Created'
        self.pre=pre
        self.container=container

    def create(self,x):
        if self.post == None:
            self.post=Chunk(self.container,self)
            newChunk =self.post
        else:
            newChunk =self.post.create(x+1)
        return newChunk

    def droprefs(self):
        print 'refcount droprefs start: ', sys.getrefcount(self)
        if not self.pre ==None:
            self.pre.droprefs()

        self.pre=None
        self.post=None
        self.container=None
        print 'refcount droprefs end: ', sys.getrefcount(self)

    def claimContainer(self):
        self.container.setmasterchunk(self)
        self.pre.droprefs()
        self.pre=None

class Container(object):
    masterchunk=None

    def __init__(self):
        self.masterchunk=Chunk(self, None)

    def setmasterchunk(self, chunk):
        print 'refcount 5: ', sys.getrefcount(self.masterchunk)
        self.masterchunk=chunk
        print 'refcount 6: ', sys.getrefcount(self.masterchunk)

    def testrecursion(self,n):

        for i in range(n):
            print 'refcount 6: ', sys.getrefcount(self.masterchunk)
            newChunk=self.masterchunk.create(1)

        return newChunk

    def testhistoryremoval(self,chunk):
        chunk.claimContainer()



if __name__ == '__main__':
    C=Container()
    middle=C.testrecursion(300)
    last=C.testrecursion(600)
    last=C.testhistoryremoval(middle)

    if middle== C.masterchunk:
        print 'SUCCESS'

补充说明:

显示最大递归深度的代码:

import sys
from time import sleep

class Chunk(object):
    pre=None
    post=None
    def __init__(self, container,pre,n):
        print 'Chunk Created'
        self.pre=pre
        self.container=container

        if n>0:
            self.post=Chunk(self.container,self,n-1)

    def droprefs(self):
        print 'refcount droprefs start: ', sys.getrefcount(self)
        if not self.pre ==None:
            self.pre.droprefs()

        self.pre=None
        self.post=None
        self.container=None
        print 'refcount droprefs end: ', sys.getrefcount(self)

    def claimContainer(self):
        self.container.setmasterchunk(self)
        self.pre.droprefs()
        self.pre=None

class Container(object):
    masterchunk=None

    def __init__(self):
        pass

    def setmasterchunk(self, chunk):
        print 'refcount 5: ', sys.getrefcount(self.masterchunk)
        self.masterchunk=chunk
        print 'refcount 6: ', sys.getrefcount(self.masterchunk)

    def testrecursion(self,n):
        self.masterchunk=Chunk(self,None,n)



if __name__ == '__main__':
    print('Try 10')
    C0=Container()
    C0.testrecursion(10)

    sleep(2)

    print('Try 1000')
    C1=Container()
    C1.testrecursion(1000)

【问题讨论】:

  • “当我将链中的下一个元素创建到管理这些对象的容器中时,问题似乎消失了”——很可能是因为您不再递归,或者递归的深度不够。
  • 您可以更改递归深度。有一种算法可以根据您计算机的性能为您计算它。
  • 信号处理不是计算密集型的吗?如果是这样,递归似乎不合适。
  • @Elazar 信号处理是计算密集型的,但我所针对的代码是将传入信息放在正确位置的机制,它先于实际的信号处理。完成并发布后,如果您有兴趣,我可以在此处发布更新。

标签: python recursion stack


【解决方案1】:

在我使用的实现中,设置

sys.setrecursionlimit(907)

证明这是您达到的递归深度。


对于您的问题,答案是否定的。在 CPython 的源码中你可以看到递归深度是每个线程,而不是每个对象。

pystate.h:

typedef struct _ts {
    /* See Python/ceval.c for comments explaining most fields */

    //...
    int recursion_depth;
    //...
}

ceval.c:

/* the macro Py_EnterRecursiveCall() only calls _Py_CheckRecursiveCall()
   if the recursion_depth reaches _Py_CheckRecursionLimit.
   If USE_STACKCHECK, the macro decrements _Py_CheckRecursionLimit
   to guarantee that _Py_CheckRecursiveCall() is regularly called.
   Without USE_STACKCHECK, there is no need for this. */
int
_Py_CheckRecursiveCall(const char *where)
{
    PyThreadState *tstate = PyThreadState_GET();
    //...
    if (tstate->recursion_depth > recursion_limit) {
        --tstate->recursion_depth;
        tstate->overflowed = 1;
        PyErr_Format(PyExc_RecursionError,
                     "maximum recursion depth exceeded%s",
                     where);
        return -1;
     }
   //...
}

tstate 是线程状态的简写。

【讨论】:

  • 这如何解释我现在添加到原始问题中的第二个示例的行为差异?线程是在一种情况下自动创建的,而不是在另一种情况下自动创建的吗?
  • @Prune 好的,我对给出的两个示例进行了一些尝试,差异似乎只是程度问题。从 init 场景的创建已经停止在 300 的“朴素递归深度”,第一个场景在 900 到 1000 之间的“朴素递归深度”停止。所以据说 python 正在添加一些函数调用实现构造函数(我认为这是有道理的。)。因此得出结论:当链变长时,递归对象链以到达链中的最后一个对象并不是一个好的解决方案。
  • @RonaldvanElburg 您回答错误。但是,我不明白您为什么认为递归 300 然后 600 会达到 1000 的最大深度。
  • 因为第二组 600 遍历了第一次调用中创建的对象,所以递归深度为 900。这仍然有效。如果我把它调到 1000,它就会失败。我认为我之前的评论证实了你和 Prune 的回答。这就是为什么我在这里发表评论,并提到了 Prune。如果有更好的方法来处理这个问题,请告诉我。
【解决方案2】:

不,这是一个全局递归深度。您描述的情况意味着当您超过堆栈限制时,您会捕获异常并继续下一个对象。这个过程会删除相关的堆栈条目——将递归解开到它的起点。

您从下一个对象重新开始。如果这个不超过堆栈深度,它将顺利完成——之前的堆栈条目不会影响新的。

【讨论】:

    【解决方案3】:

    您可以将方法重写为迭代而不是递归。无论您的数据结构有多深,这都避免了递归深度错误的可能性。

    create 方法会变成这样:

    def create(self, x): # the `x` arg is not actually used for anything?
        chunk = self
    
        while chunk.post is not None:
            chunk = chunk.post
    
        chunk.post = Chunk(self.container, chunk)
    
        return self.post # this matches the old code but you may actually want `return self.post`
    

    你可以用droprefs 做类似的事情。您当前的代码似乎从列表的末尾向前迭代,这可能是也可能不是您想要的。这是对迭代行为的直接翻译:

    def droprefs(self):
        chunk = self
    
        while chunk:
            next = chunk.pre # this iterates backwards, to go forwards use `next = chunk.post`
    
            chunk.pre = None
            chunk.post = None
            chunk.container = None
    
            chunk = next
    

    我还要注意,除非您希望外部代码包含对早期 Chunks 的引用,否则您可能根本不需要使用 droprefs。在claimContainer 执行self.pre = None 之后,垃圾收集器应该能够清理所有早期的Chunk 实例,因为不再有对它们的外部引用。摆脱它们对彼此的引用可能会使其工作得更快(因为 prepost 属性形成引用循环),但这并不是绝对必要的。

    【讨论】:

    • 这是我正在计划的解决方案,但它需要大量工作并且可能会破坏我的代码。在开始这次冒险之前,我想确保我充分理解了调用堆栈。原始代码已经有很多引用删除,以允许垃圾收集器完成它的工作。
    猜你喜欢
    • 2012-12-22
    • 2018-05-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-03
    • 2017-11-07
    • 2011-10-12
    • 1970-01-01
    相关资源
    最近更新 更多