关于递归:每个对象的python最大重复深度是多少?

Is python maximum recurrence depth per object?

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

对于该错误最可能的解释是,有时单个类的大量实例是从类的init方法递归创建的。实际上,模拟这种情况的实验会导致一个异常的runtimeerror:"超过了最大递归深度"。

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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'

添加的注释:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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)


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

create方法会变成这样:

1
2
3
4
5
6
7
8
9
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做类似的事情。您当前的代码似乎是从列表的末尾向前迭代的,这可能是您想要的,也可能不是您想要的。下面是对迭代行为的直接转换:

1
2
3
4
5
6
7
8
9
10
11
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

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


在我使用的实现中,设置

1
sys.setrecursionlimit(907)

演示这是您达到的递归深度。

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

PySt.H:

1
2
3
4
5
6
7
typedef struct _ts {
    /* See Python/ceval.c for comments explaining most fields */

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

C:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 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是线程状态的简写。


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

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